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 Mapping, Dict, Any
16 from nominatim.tools.exec_utils import run_api_script
17 from nominatim.errors import UsageError
18 from nominatim.clicmd.args import NominatimArgs
19 import nominatim.api as napi
20 import nominatim.api.v1 as api_output
21 from nominatim.api.v1.helpers import zoom_to_rank, deduplicate_results
22 import nominatim.api.logging as loglib
24 # Do not repeat documentation of subcommand classes.
25 # pylint: disable=C0111
27 LOG = logging.getLogger()
30 ('amenity', 'name and/or type of POI'),
31 ('street', 'housenumber and street'),
32 ('city', 'city, town or village'),
35 ('country', 'country'),
36 ('postalcode', 'postcode')
40 ('addressdetails', 'Include a breakdown of the address into elements'),
41 ('extratags', ("Include additional information if available "
42 "(e.g. wikipedia link, opening hours)")),
43 ('namedetails', 'Include a list of alternative names')
46 def _add_api_output_arguments(parser: argparse.ArgumentParser) -> None:
47 group = parser.add_argument_group('Output arguments')
48 group.add_argument('--format', default='jsonv2',
49 choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson', 'debug'],
50 help='Format of result')
51 for name, desc in EXTRADATA_PARAMS:
52 group.add_argument('--' + name, action='store_true', help=desc)
54 group.add_argument('--lang', '--accept-language', metavar='LANGS',
55 help='Preferred language order for presenting search results')
56 group.add_argument('--polygon-output',
57 choices=['geojson', 'kml', 'svg', 'text'],
58 help='Output geometry of results as a GeoJSON, KML, SVG or WKT')
59 group.add_argument('--polygon-threshold', type=float, default = 0.0,
61 help=("Simplify output geometry."
62 "Parameter is difference tolerance in degrees."))
65 def _run_api(endpoint: str, args: NominatimArgs, params: Mapping[str, object]) -> int:
66 script_file = args.project_dir / 'website' / (endpoint + '.php')
68 if not script_file.exists():
69 LOG.error("Cannot find API script file.\n\n"
70 "Make sure to run 'nominatim' from the project directory \n"
71 "or use the option --project-dir.")
72 raise UsageError("API script not found.")
74 return run_api_script(endpoint, args.project_dir,
75 phpcgi_bin=args.phpcgi_path, params=params)
79 Execute a search query.
81 This command works exactly the same as if calling the /search endpoint on
82 the web API. See the online documentation for more details on the
84 https://nominatim.org/release-docs/latest/api/Search/
87 def add_args(self, parser: argparse.ArgumentParser) -> None:
88 group = parser.add_argument_group('Query arguments')
89 group.add_argument('--query',
90 help='Free-form query string')
91 for name, desc in STRUCTURED_QUERY:
92 group.add_argument('--' + name, help='Structured query: ' + desc)
94 _add_api_output_arguments(parser)
96 group = parser.add_argument_group('Result limitation')
97 group.add_argument('--countrycodes', metavar='CC,..',
98 help='Limit search results to one or more countries')
99 group.add_argument('--exclude_place_ids', metavar='ID,..',
100 help='List of search object to be excluded')
101 group.add_argument('--limit', type=int, default=10,
102 help='Limit the number of returned results')
103 group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
104 help='Preferred area to find search results')
105 group.add_argument('--bounded', action='store_true',
106 help='Strictly restrict results to viewbox area')
108 group = parser.add_argument_group('Other arguments')
109 group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
110 help='Do not remove duplicates from the result list')
113 def run(self, args: NominatimArgs) -> int:
114 if args.format == 'debug':
115 loglib.set_log_output('text')
117 api = napi.NominatimAPI(args.project_dir)
119 params: Dict[str, Any] = {'max_results': args.limit + min(args.limit, 10),
120 'address_details': True, # needed for display name
121 'geometry_output': args.get_geometry_output(),
122 'geometry_simplification': args.polygon_threshold,
123 'countries': args.countrycodes,
124 'excluded': args.exclude_place_ids,
125 'viewbox': args.viewbox,
126 'bounded_viewbox': args.bounded
130 results = api.search(args.query, **params)
132 results = api.search_address(amenity=args.amenity,
137 postalcode=args.postalcode,
138 country=args.country,
141 for result in results:
142 result.localize(args.get_locales(api.config.DEFAULT_LANGUAGE))
144 if args.dedupe and len(results) > 1:
145 results = deduplicate_results(results, args.limit)
147 if args.format == 'debug':
148 print(loglib.get_and_disable())
151 output = api_output.format_result(
154 {'extratags': args.extratags,
155 'namedetails': args.namedetails,
156 'addressdetails': args.addressdetails})
157 if args.format != 'xml':
158 # reformat the result, so it is pretty-printed
159 json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
161 sys.stdout.write(output)
162 sys.stdout.write('\n')
169 Execute API reverse query.
171 This command works exactly the same as if calling the /reverse endpoint on
172 the web API. See the online documentation for more details on the
174 https://nominatim.org/release-docs/latest/api/Reverse/
177 def add_args(self, parser: argparse.ArgumentParser) -> None:
178 group = parser.add_argument_group('Query arguments')
179 group.add_argument('--lat', type=float, required=True,
180 help='Latitude of coordinate to look up (in WGS84)')
181 group.add_argument('--lon', type=float, required=True,
182 help='Longitude of coordinate to look up (in WGS84)')
183 group.add_argument('--zoom', type=int,
184 help='Level of detail required for the address')
185 group.add_argument('--layer', metavar='LAYER',
186 choices=[n.name.lower() for n in napi.DataLayer if n.name],
187 action='append', required=False, dest='layers',
188 help='OSM id to lookup in format <NRW><id> (may be repeated)')
190 _add_api_output_arguments(parser)
193 def run(self, args: NominatimArgs) -> int:
194 if args.format == 'debug':
195 loglib.set_log_output('text')
197 api = napi.NominatimAPI(args.project_dir)
199 result = api.reverse(napi.Point(args.lon, args.lat),
200 max_rank=zoom_to_rank(args.zoom or 18),
201 layers=args.get_layers(napi.DataLayer.ADDRESS | napi.DataLayer.POI),
202 address_details=True, # needed for display name
203 geometry_output=args.get_geometry_output(),
204 geometry_simplification=args.polygon_threshold)
206 if args.format == 'debug':
207 print(loglib.get_and_disable())
211 result.localize(args.get_locales(api.config.DEFAULT_LANGUAGE))
212 output = api_output.format_result(
213 napi.ReverseResults([result]),
215 {'extratags': args.extratags,
216 'namedetails': args.namedetails,
217 'addressdetails': args.addressdetails})
218 if args.format != 'xml':
219 # reformat the result, so it is pretty-printed
220 json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
222 sys.stdout.write(output)
223 sys.stdout.write('\n')
227 LOG.error("Unable to geocode.")
234 Execute API lookup query.
236 This command works exactly the same as if calling the /lookup endpoint on
237 the web API. See the online documentation for more details on the
239 https://nominatim.org/release-docs/latest/api/Lookup/
242 def add_args(self, parser: argparse.ArgumentParser) -> None:
243 group = parser.add_argument_group('Query arguments')
244 group.add_argument('--id', metavar='OSMID',
245 action='append', required=True, dest='ids',
246 help='OSM id to lookup in format <NRW><id> (may be repeated)')
248 _add_api_output_arguments(parser)
251 def run(self, args: NominatimArgs) -> int:
252 if args.format == 'debug':
253 loglib.set_log_output('text')
255 api = napi.NominatimAPI(args.project_dir)
257 if args.format == 'debug':
258 print(loglib.get_and_disable())
261 places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids]
263 results = api.lookup(places,
264 address_details=True, # needed for display name
265 geometry_output=args.get_geometry_output(),
266 geometry_simplification=args.polygon_threshold or 0.0)
268 for result in results:
269 result.localize(args.get_locales(api.config.DEFAULT_LANGUAGE))
271 output = api_output.format_result(
274 {'extratags': args.extratags,
275 'namedetails': args.namedetails,
276 'addressdetails': args.addressdetails})
277 if args.format != 'xml':
278 # reformat the result, so it is pretty-printed
279 json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
281 sys.stdout.write(output)
282 sys.stdout.write('\n')
289 Execute API details query.
291 This command works exactly the same as if calling the /details endpoint on
292 the web API. See the online documentation for more details on the
294 https://nominatim.org/release-docs/latest/api/Details/
297 def add_args(self, parser: argparse.ArgumentParser) -> None:
298 group = parser.add_argument_group('Query arguments')
299 objs = group.add_mutually_exclusive_group(required=True)
300 objs.add_argument('--node', '-n', type=int,
301 help="Look up the OSM node with the given ID.")
302 objs.add_argument('--way', '-w', type=int,
303 help="Look up the OSM way with the given ID.")
304 objs.add_argument('--relation', '-r', type=int,
305 help="Look up the OSM relation with the given ID.")
306 objs.add_argument('--place_id', '-p', type=int,
307 help='Database internal identifier of the OSM object to look up')
308 group.add_argument('--class', dest='object_class',
309 help=("Class type to disambiguated multiple entries "
310 "of the same object."))
312 group = parser.add_argument_group('Output arguments')
313 group.add_argument('--addressdetails', action='store_true',
314 help='Include a breakdown of the address into elements')
315 group.add_argument('--keywords', action='store_true',
316 help='Include a list of name keywords and address keywords')
317 group.add_argument('--linkedplaces', action='store_true',
318 help='Include a details of places that are linked with this one')
319 group.add_argument('--hierarchy', action='store_true',
320 help='Include details of places lower in the address hierarchy')
321 group.add_argument('--group_hierarchy', action='store_true',
322 help='Group the places by type')
323 group.add_argument('--polygon_geojson', action='store_true',
324 help='Include geometry of result')
325 group.add_argument('--lang', '--accept-language', metavar='LANGS',
326 help='Preferred language order for presenting search results')
329 def run(self, args: NominatimArgs) -> int:
332 place = napi.OsmID('N', args.node, args.object_class)
334 place = napi.OsmID('W', args.way, args.object_class)
336 place = napi.OsmID('R', args.relation, args.object_class)
338 assert args.place_id is not None
339 place = napi.PlaceID(args.place_id)
341 api = napi.NominatimAPI(args.project_dir)
343 result = api.details(place,
344 address_details=args.addressdetails,
345 linked_places=args.linkedplaces,
346 parented_places=args.hierarchy,
347 keywords=args.keywords,
348 geometry_output=napi.GeometryFormat.GEOJSON
349 if args.polygon_geojson
350 else napi.GeometryFormat.NONE)
354 locales = args.get_locales(api.config.DEFAULT_LANGUAGE)
355 result.localize(locales)
357 output = api_output.format_result(
361 'group_hierarchy': args.group_hierarchy})
362 # reformat the result, so it is pretty-printed
363 json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
364 sys.stdout.write('\n')
368 LOG.error("Object not found in database.")
374 Execute API status query.
376 This command works exactly the same as if calling the /status endpoint on
377 the web API. See the online documentation for more details on the
379 https://nominatim.org/release-docs/latest/api/Status/
382 def add_args(self, parser: argparse.ArgumentParser) -> None:
383 formats = api_output.list_formats(napi.StatusResult)
384 group = parser.add_argument_group('API parameters')
385 group.add_argument('--format', default=formats[0], choices=formats,
386 help='Format of result')
389 def run(self, args: NominatimArgs) -> int:
390 status = napi.NominatimAPI(args.project_dir).status()
391 print(api_output.format_result(status, args.format, {}))