]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/clicmd/api.py
Merge pull request #3206 from lonvia/rerank-by-locale
[nominatim.git] / nominatim / clicmd / api.py
1 # SPDX-License-Identifier: GPL-2.0-only
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2023 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Subcommand definitions for API calls from the command line.
9 """
10 from typing import Dict, Any
11 import argparse
12 import logging
13 import json
14 import sys
15
16 from nominatim.clicmd.args import NominatimArgs
17 import nominatim.api as napi
18 import nominatim.api.v1 as api_output
19 from nominatim.api.v1.helpers import zoom_to_rank, deduplicate_results
20 import nominatim.api.logging as loglib
21
22 # Do not repeat documentation of subcommand classes.
23 # pylint: disable=C0111
24
25 LOG = logging.getLogger()
26
27 STRUCTURED_QUERY = (
28     ('amenity', 'name and/or type of POI'),
29     ('street', 'housenumber and street'),
30     ('city', 'city, town or village'),
31     ('county', 'county'),
32     ('state', 'state'),
33     ('country', 'country'),
34     ('postalcode', 'postcode')
35 )
36
37 EXTRADATA_PARAMS = (
38     ('addressdetails', 'Include a breakdown of the address into elements'),
39     ('extratags', ("Include additional information if available "
40                    "(e.g. wikipedia link, opening hours)")),
41     ('namedetails', 'Include a list of alternative names')
42 )
43
44 def _add_api_output_arguments(parser: argparse.ArgumentParser) -> None:
45     group = parser.add_argument_group('Output arguments')
46     group.add_argument('--format', default='jsonv2',
47                        choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson', 'debug'],
48                        help='Format of result')
49     for name, desc in EXTRADATA_PARAMS:
50         group.add_argument('--' + name, action='store_true', help=desc)
51
52     group.add_argument('--lang', '--accept-language', metavar='LANGS',
53                        help='Preferred language order for presenting search results')
54     group.add_argument('--polygon-output',
55                        choices=['geojson', 'kml', 'svg', 'text'],
56                        help='Output geometry of results as a GeoJSON, KML, SVG or WKT')
57     group.add_argument('--polygon-threshold', type=float, default = 0.0,
58                        metavar='TOLERANCE',
59                        help=("Simplify output geometry."
60                              "Parameter is difference tolerance in degrees."))
61
62
63 class APISearch:
64     """\
65     Execute a search query.
66
67     This command works exactly the same as if calling the /search endpoint on
68     the web API. See the online documentation for more details on the
69     various parameters:
70     https://nominatim.org/release-docs/latest/api/Search/
71     """
72
73     def add_args(self, parser: argparse.ArgumentParser) -> None:
74         group = parser.add_argument_group('Query arguments')
75         group.add_argument('--query',
76                            help='Free-form query string')
77         for name, desc in STRUCTURED_QUERY:
78             group.add_argument('--' + name, help='Structured query: ' + desc)
79
80         _add_api_output_arguments(parser)
81
82         group = parser.add_argument_group('Result limitation')
83         group.add_argument('--countrycodes', metavar='CC,..',
84                            help='Limit search results to one or more countries')
85         group.add_argument('--exclude_place_ids', metavar='ID,..',
86                            help='List of search object to be excluded')
87         group.add_argument('--limit', type=int, default=10,
88                            help='Limit the number of returned results')
89         group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
90                            help='Preferred area to find search results')
91         group.add_argument('--bounded', action='store_true',
92                            help='Strictly restrict results to viewbox area')
93
94         group = parser.add_argument_group('Other arguments')
95         group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
96                            help='Do not remove duplicates from the result list')
97
98
99     def run(self, args: NominatimArgs) -> int:
100         if args.format == 'debug':
101             loglib.set_log_output('text')
102
103         api = napi.NominatimAPI(args.project_dir)
104
105         params: Dict[str, Any] = {'max_results': args.limit + min(args.limit, 10),
106                                   'address_details': True, # needed for display name
107                                   'geometry_output': args.get_geometry_output(),
108                                   'geometry_simplification': args.polygon_threshold,
109                                   'countries': args.countrycodes,
110                                   'excluded': args.exclude_place_ids,
111                                   'viewbox': args.viewbox,
112                                   'bounded_viewbox': args.bounded,
113                                   'locales': args.get_locales(api.config.DEFAULT_LANGUAGE)
114                                  }
115
116         if args.query:
117             results = api.search(args.query, **params)
118         else:
119             results = api.search_address(amenity=args.amenity,
120                                          street=args.street,
121                                          city=args.city,
122                                          county=args.county,
123                                          state=args.state,
124                                          postalcode=args.postalcode,
125                                          country=args.country,
126                                          **params)
127
128         if args.dedupe and len(results) > 1:
129             results = deduplicate_results(results, args.limit)
130
131         if args.format == 'debug':
132             print(loglib.get_and_disable())
133             return 0
134
135         output = api_output.format_result(
136                     results,
137                     args.format,
138                     {'extratags': args.extratags,
139                      'namedetails': args.namedetails,
140                      'addressdetails': args.addressdetails})
141         if args.format != 'xml':
142             # reformat the result, so it is pretty-printed
143             json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
144         else:
145             sys.stdout.write(output)
146         sys.stdout.write('\n')
147
148         return 0
149
150
151 class APIReverse:
152     """\
153     Execute API reverse query.
154
155     This command works exactly the same as if calling the /reverse endpoint on
156     the web API. See the online documentation for more details on the
157     various parameters:
158     https://nominatim.org/release-docs/latest/api/Reverse/
159     """
160
161     def add_args(self, parser: argparse.ArgumentParser) -> None:
162         group = parser.add_argument_group('Query arguments')
163         group.add_argument('--lat', type=float, required=True,
164                            help='Latitude of coordinate to look up (in WGS84)')
165         group.add_argument('--lon', type=float, required=True,
166                            help='Longitude of coordinate to look up (in WGS84)')
167         group.add_argument('--zoom', type=int,
168                            help='Level of detail required for the address')
169         group.add_argument('--layer', metavar='LAYER',
170                            choices=[n.name.lower() for n in napi.DataLayer if n.name],
171                            action='append', required=False, dest='layers',
172                            help='OSM id to lookup in format <NRW><id> (may be repeated)')
173
174         _add_api_output_arguments(parser)
175
176
177     def run(self, args: NominatimArgs) -> int:
178         if args.format == 'debug':
179             loglib.set_log_output('text')
180
181         api = napi.NominatimAPI(args.project_dir)
182
183         result = api.reverse(napi.Point(args.lon, args.lat),
184                              max_rank=zoom_to_rank(args.zoom or 18),
185                              layers=args.get_layers(napi.DataLayer.ADDRESS | napi.DataLayer.POI),
186                              address_details=True, # needed for display name
187                              geometry_output=args.get_geometry_output(),
188                              geometry_simplification=args.polygon_threshold,
189                              locales=args.get_locales(api.config.DEFAULT_LANGUAGE))
190
191         if args.format == 'debug':
192             print(loglib.get_and_disable())
193             return 0
194
195         if result:
196             output = api_output.format_result(
197                         napi.ReverseResults([result]),
198                         args.format,
199                         {'extratags': args.extratags,
200                          'namedetails': args.namedetails,
201                          'addressdetails': args.addressdetails})
202             if args.format != 'xml':
203                 # reformat the result, so it is pretty-printed
204                 json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
205             else:
206                 sys.stdout.write(output)
207             sys.stdout.write('\n')
208
209             return 0
210
211         LOG.error("Unable to geocode.")
212         return 42
213
214
215
216 class APILookup:
217     """\
218     Execute API lookup query.
219
220     This command works exactly the same as if calling the /lookup endpoint on
221     the web API. See the online documentation for more details on the
222     various parameters:
223     https://nominatim.org/release-docs/latest/api/Lookup/
224     """
225
226     def add_args(self, parser: argparse.ArgumentParser) -> None:
227         group = parser.add_argument_group('Query arguments')
228         group.add_argument('--id', metavar='OSMID',
229                            action='append', required=True, dest='ids',
230                            help='OSM id to lookup in format <NRW><id> (may be repeated)')
231
232         _add_api_output_arguments(parser)
233
234
235     def run(self, args: NominatimArgs) -> int:
236         if args.format == 'debug':
237             loglib.set_log_output('text')
238
239         api = napi.NominatimAPI(args.project_dir)
240
241         if args.format == 'debug':
242             print(loglib.get_and_disable())
243             return 0
244
245         places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids]
246
247         results = api.lookup(places,
248                              address_details=True, # needed for display name
249                              geometry_output=args.get_geometry_output(),
250                              geometry_simplification=args.polygon_threshold or 0.0,
251                              locales=args.get_locales(api.config.DEFAULT_LANGUAGE))
252
253         output = api_output.format_result(
254                     results,
255                     args.format,
256                     {'extratags': args.extratags,
257                      'namedetails': args.namedetails,
258                      'addressdetails': args.addressdetails})
259         if args.format != 'xml':
260             # reformat the result, so it is pretty-printed
261             json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
262         else:
263             sys.stdout.write(output)
264         sys.stdout.write('\n')
265
266         return 0
267
268
269 class APIDetails:
270     """\
271     Execute API details query.
272
273     This command works exactly the same as if calling the /details endpoint on
274     the web API. See the online documentation for more details on the
275     various parameters:
276     https://nominatim.org/release-docs/latest/api/Details/
277     """
278
279     def add_args(self, parser: argparse.ArgumentParser) -> None:
280         group = parser.add_argument_group('Query arguments')
281         objs = group.add_mutually_exclusive_group(required=True)
282         objs.add_argument('--node', '-n', type=int,
283                           help="Look up the OSM node with the given ID.")
284         objs.add_argument('--way', '-w', type=int,
285                           help="Look up the OSM way with the given ID.")
286         objs.add_argument('--relation', '-r', type=int,
287                           help="Look up the OSM relation with the given ID.")
288         objs.add_argument('--place_id', '-p', type=int,
289                           help='Database internal identifier of the OSM object to look up')
290         group.add_argument('--class', dest='object_class',
291                            help=("Class type to disambiguated multiple entries "
292                                  "of the same object."))
293
294         group = parser.add_argument_group('Output arguments')
295         group.add_argument('--addressdetails', action='store_true',
296                            help='Include a breakdown of the address into elements')
297         group.add_argument('--keywords', action='store_true',
298                            help='Include a list of name keywords and address keywords')
299         group.add_argument('--linkedplaces', action='store_true',
300                            help='Include a details of places that are linked with this one')
301         group.add_argument('--hierarchy', action='store_true',
302                            help='Include details of places lower in the address hierarchy')
303         group.add_argument('--group_hierarchy', action='store_true',
304                            help='Group the places by type')
305         group.add_argument('--polygon_geojson', action='store_true',
306                            help='Include geometry of result')
307         group.add_argument('--lang', '--accept-language', metavar='LANGS',
308                            help='Preferred language order for presenting search results')
309
310
311     def run(self, args: NominatimArgs) -> int:
312         place: napi.PlaceRef
313         if args.node:
314             place = napi.OsmID('N', args.node, args.object_class)
315         elif args.way:
316             place = napi.OsmID('W', args.way, args.object_class)
317         elif args.relation:
318             place = napi.OsmID('R', args.relation, args.object_class)
319         else:
320             assert args.place_id is not None
321             place = napi.PlaceID(args.place_id)
322
323         api = napi.NominatimAPI(args.project_dir)
324
325         locales = args.get_locales(api.config.DEFAULT_LANGUAGE)
326         result = api.details(place,
327                              address_details=args.addressdetails,
328                              linked_places=args.linkedplaces,
329                              parented_places=args.hierarchy,
330                              keywords=args.keywords,
331                              geometry_output=napi.GeometryFormat.GEOJSON
332                                              if args.polygon_geojson
333                                              else napi.GeometryFormat.NONE,
334                             locales=locales)
335
336
337         if result:
338             output = api_output.format_result(
339                         result,
340                         'json',
341                         {'locales': locales,
342                          'group_hierarchy': args.group_hierarchy})
343             # reformat the result, so it is pretty-printed
344             json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
345             sys.stdout.write('\n')
346
347             return 0
348
349         LOG.error("Object not found in database.")
350         return 42
351
352
353 class APIStatus:
354     """
355     Execute API status query.
356
357     This command works exactly the same as if calling the /status endpoint on
358     the web API. See the online documentation for more details on the
359     various parameters:
360     https://nominatim.org/release-docs/latest/api/Status/
361     """
362
363     def add_args(self, parser: argparse.ArgumentParser) -> None:
364         formats = api_output.list_formats(napi.StatusResult)
365         group = parser.add_argument_group('API parameters')
366         group.add_argument('--format', default=formats[0], choices=formats,
367                            help='Format of result')
368
369
370     def run(self, args: NominatimArgs) -> int:
371         status = napi.NominatimAPI(args.project_dir).status()
372         print(api_output.format_result(status, args.format, {}))
373         return 0