1 # SPDX-License-Identifier: GPL-2.0-only
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2023 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Subcommand definitions for API calls from the command line.
10 from typing import Dict, Any
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
22 # Do not repeat documentation of subcommand classes.
23 # pylint: disable=C0111
25 LOG = logging.getLogger()
28 ('amenity', 'name and/or type of POI'),
29 ('street', 'housenumber and street'),
30 ('city', 'city, town or village'),
33 ('country', 'country'),
34 ('postalcode', 'postcode')
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')
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)
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,
59 help=("Simplify output geometry."
60 "Parameter is difference tolerance in degrees."))
65 Execute a search query.
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
70 https://nominatim.org/release-docs/latest/api/Search/
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)
80 _add_api_output_arguments(parser)
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')
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')
99 def run(self, args: NominatimArgs) -> int:
100 if args.format == 'debug':
101 loglib.set_log_output('text')
103 api = napi.NominatimAPI(args.project_dir)
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)
117 results = api.search(args.query, **params)
119 results = api.search_address(amenity=args.amenity,
124 postalcode=args.postalcode,
125 country=args.country,
128 if args.dedupe and len(results) > 1:
129 results = deduplicate_results(results, args.limit)
131 if args.format == 'debug':
132 print(loglib.get_and_disable())
135 output = api_output.format_result(
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)
145 sys.stdout.write(output)
146 sys.stdout.write('\n')
153 Execute API reverse query.
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
158 https://nominatim.org/release-docs/latest/api/Reverse/
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)')
174 _add_api_output_arguments(parser)
177 def run(self, args: NominatimArgs) -> int:
178 if args.format == 'debug':
179 loglib.set_log_output('text')
181 api = napi.NominatimAPI(args.project_dir)
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))
191 if args.format == 'debug':
192 print(loglib.get_and_disable())
196 output = api_output.format_result(
197 napi.ReverseResults([result]),
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)
206 sys.stdout.write(output)
207 sys.stdout.write('\n')
211 LOG.error("Unable to geocode.")
218 Execute API lookup query.
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
223 https://nominatim.org/release-docs/latest/api/Lookup/
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)')
232 _add_api_output_arguments(parser)
235 def run(self, args: NominatimArgs) -> int:
236 if args.format == 'debug':
237 loglib.set_log_output('text')
239 api = napi.NominatimAPI(args.project_dir)
241 if args.format == 'debug':
242 print(loglib.get_and_disable())
245 places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids]
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))
253 output = api_output.format_result(
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)
263 sys.stdout.write(output)
264 sys.stdout.write('\n')
271 Execute API details query.
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
276 https://nominatim.org/release-docs/latest/api/Details/
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."))
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')
311 def run(self, args: NominatimArgs) -> int:
314 place = napi.OsmID('N', args.node, args.object_class)
316 place = napi.OsmID('W', args.way, args.object_class)
318 place = napi.OsmID('R', args.relation, args.object_class)
320 assert args.place_id is not None
321 place = napi.PlaceID(args.place_id)
323 api = napi.NominatimAPI(args.project_dir)
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,
338 output = api_output.format_result(
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')
349 LOG.error("Object not found in database.")
355 Execute API status query.
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
360 https://nominatim.org/release-docs/latest/api/Status/
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')
370 def run(self, args: NominatimArgs) -> int:
371 status = napi.NominatimAPI(args.project_dir).status()
372 print(api_output.format_result(status, args.format, {}))