2 Command-line interface to the Nominatim functions for import, update,
3 database administration and querying.
9 from pathlib import Path
11 from .config import Configuration
12 from .tools.exec_utils import run_legacy_script, run_api_script
13 from .db.connection import connect
15 LOG = logging.getLogger()
17 def _num_system_cpus():
19 cpus = len(os.sched_getaffinity(0))
20 except NotImplementedError:
23 return cpus or os.cpu_count()
26 class CommandlineParser:
27 """ Wraps some of the common functions for parsing the command line
28 and setting up subcommands.
30 def __init__(self, prog, description):
31 self.parser = argparse.ArgumentParser(
33 description=description,
34 formatter_class=argparse.RawDescriptionHelpFormatter)
36 self.subs = self.parser.add_subparsers(title='available commands',
39 # Arguments added to every sub-command
40 self.default_args = argparse.ArgumentParser(add_help=False)
41 group = self.default_args.add_argument_group('Default arguments')
42 group.add_argument('-h', '--help', action='help',
43 help='Show this help message and exit')
44 group.add_argument('-q', '--quiet', action='store_const', const=0,
45 dest='verbose', default=1,
46 help='Print only error messages')
47 group.add_argument('-v', '--verbose', action='count', default=1,
48 help='Increase verboseness of output')
49 group.add_argument('--project-dir', metavar='DIR', default='.',
50 help='Base directory of the Nominatim installation (default:.)')
51 group.add_argument('-j', '--threads', metavar='NUM', type=int,
52 help='Number of parallel threads to use')
55 def add_subcommand(self, name, cmd):
56 """ Add a subcommand to the parser. The subcommand must be a class
57 with a function add_args() that adds the parameters for the
58 subcommand and a run() function that executes the command.
60 parser = self.subs.add_parser(name, parents=[self.default_args],
61 help=cmd.__doc__.split('\n', 1)[0],
62 description=cmd.__doc__,
63 formatter_class=argparse.RawDescriptionHelpFormatter,
65 parser.set_defaults(command=cmd)
68 def run(self, **kwargs):
69 """ Parse the command line arguments of the program and execute the
70 appropriate subcommand.
72 args = self.parser.parse_args(args=kwargs.get('cli_args'))
74 if args.subcommand is None:
75 self.parser.print_help()
78 for arg in ('module_dir', 'osm2pgsql_path', 'phplib_dir', 'data_dir', 'phpcgi_path'):
79 setattr(args, arg, Path(kwargs[arg]))
80 args.project_dir = Path(args.project_dir)
82 logging.basicConfig(stream=sys.stderr,
83 format='%(asctime)s: %(message)s',
84 datefmt='%Y-%m-%d %H:%M:%S',
85 level=max(4 - args.verbose, 1) * 10)
87 args.config = Configuration(args.project_dir, args.data_dir / 'settings')
89 return args.command.run(args)
91 ##### Subcommand classes
93 # Each class needs to implement two functions: add_args() adds the CLI parameters
94 # for the subfunction, run() executes the subcommand.
96 # The class documentation doubles as the help text for the command. The
97 # first line is also used in the summary when calling the program without
100 # No need to document the functions each time.
101 # pylint: disable=C0111
106 Create a new Nominatim database from an OSM file.
110 def add_args(parser):
111 group_name = parser.add_argument_group('Required arguments')
112 group = group_name.add_mutually_exclusive_group(required=True)
113 group.add_argument('--osm-file',
114 help='OSM file to be imported.')
115 group.add_argument('--continue', dest='continue_at',
116 choices=['load-data', 'indexing', 'db-postprocess'],
117 help='Continue an import that was interrupted')
118 group = parser.add_argument_group('Optional arguments')
119 group.add_argument('--osm2pgsql-cache', metavar='SIZE', type=int,
120 help='Size of cache to be used by osm2pgsql (in MB)')
121 group.add_argument('--reverse-only', action='store_true',
122 help='Do not create tables and indexes for searching')
123 group.add_argument('--enable-debug-statements', action='store_true',
124 help='Include debug warning statements in SQL code')
125 group.add_argument('--no-partitions', action='store_true',
126 help="""Do not partition search indices
127 (speeds up import of single country extracts)""")
128 group.add_argument('--no-updates', action='store_true',
129 help="""Do not keep tables that are only needed for
130 updating the database later""")
131 group = parser.add_argument_group('Expert options')
132 group.add_argument('--ignore-errors', action='store_true',
133 help='Continue import even when errors in SQL are present')
134 group.add_argument('--index-noanalyse', action='store_true',
135 help='Do not perform analyse operations during index')
140 params = ['setup.php']
142 params.extend(('--all', '--osm-file', args.osm_file))
144 if args.continue_at == 'load-data':
145 params.append('--load-data')
146 if args.continue_at in ('load-data', 'indexing'):
147 params.append('--index')
148 params.extend(('--create-search-indices', '--create-country-names',
150 if args.osm2pgsql_cache:
151 params.extend(('--osm2pgsql-cache', args.osm2pgsql_cache))
152 if args.reverse_only:
153 params.append('--reverse-only')
154 if args.enable_debug_statements:
155 params.append('--enable-debug-statements')
156 if args.no_partitions:
157 params.append('--no-partitions')
159 params.append('--drop')
160 if args.ignore_errors:
161 params.append('--ignore-errors')
162 if args.index_noanalyse:
163 params.append('--index-noanalyse')
165 return run_legacy_script(*params, nominatim_env=args)
170 Make database read-only.
172 About half of data in the Nominatim database is kept only to be able to
173 keep the data up-to-date with new changes made in OpenStreetMap. This
174 command drops all this data and only keeps the part needed for geocoding
177 This command has the same effect as the `--no-updates` option for imports.
181 def add_args(parser):
186 return run_legacy_script('setup.php', '--drop', nominatim_env=args)
189 class SetupSpecialPhrases:
191 Maintain special phrases.
195 def add_args(parser):
196 group = parser.add_argument_group('Input arguments')
197 group.add_argument('--from-wiki', action='store_true',
198 help='Pull special phrases from the OSM wiki.')
199 group = parser.add_argument_group('Output arguments')
200 group.add_argument('-o', '--output', default='-',
201 help="""File to write the preprocessed phrases to.
202 If omitted, it will be written to stdout.""")
206 if args.output != '-':
207 raise NotImplementedError('Only output to stdout is currently implemented.')
208 return run_legacy_script('specialphrases.php', '--wiki-import', nominatim_env=args)
211 class UpdateReplication:
213 Update the database using an online replication service.
217 def add_args(parser):
218 group = parser.add_argument_group('Arguments for initialisation')
219 group.add_argument('--init', action='store_true',
220 help='Initialise the update process')
221 group.add_argument('--no-update-functions', dest='update_functions',
222 action='store_false',
223 help="""Do not update the trigger function to
224 support differential updates.""")
225 group = parser.add_argument_group('Arguments for updates')
226 group.add_argument('--check-for-updates', action='store_true',
227 help='Check if new updates are available and exit')
228 group.add_argument('--once', action='store_true',
229 help="""Download and apply updates only once. When
230 not set, updates are continuously applied""")
231 group.add_argument('--no-index', action='store_false', dest='do_index',
232 help="""Do not index the new data. Only applicable
233 together with --once""")
237 params = ['update.php']
239 params.append('--init-updates')
240 if not args.update_functions:
241 params.append('--no-update-functions')
242 elif args.check_for_updates:
243 params.append('--check-for-updates')
246 params.append('--import-osmosis')
248 params.append('--import-osmosis-all')
249 if not args.do_index:
250 params.append('--no-index')
252 return run_legacy_script(*params, nominatim_env=args)
257 Add additional data from a file or an online source.
259 Data is only imported, not indexed. You need to call `nominatim-update index`
260 to complete the process.
264 def add_args(parser):
265 group_name = parser.add_argument_group('Source')
266 group = group_name.add_mutually_exclusive_group(required=True)
267 group.add_argument('--file', metavar='FILE',
268 help='Import data from an OSM file')
269 group.add_argument('--diff', metavar='FILE',
270 help='Import data from an OSM diff file')
271 group.add_argument('--node', metavar='ID', type=int,
272 help='Import a single node from the API')
273 group.add_argument('--way', metavar='ID', type=int,
274 help='Import a single way from the API')
275 group.add_argument('--relation', metavar='ID', type=int,
276 help='Import a single relation from the API')
277 group.add_argument('--tiger-data', metavar='DIR',
278 help='Add housenumbers from the US TIGER census database.')
279 group = parser.add_argument_group('Extra arguments')
280 group.add_argument('--use-main-api', action='store_true',
281 help='Use OSM API instead of Overpass to download objects')
286 os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
287 return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
289 params = ['update.php']
291 params.extend(('--import-file', args.file))
293 params.extend(('--import-diff', args.diff))
295 params.extend(('--import-node', args.node))
297 params.extend(('--import-way', args.way))
299 params.extend(('--import-relation', args.relation))
300 if args.use_main_api:
301 params.append('--use-main-api')
302 return run_legacy_script(*params, nominatim_env=args)
307 Reindex all new and modified data.
311 def add_args(parser):
312 group = parser.add_argument_group('Filter arguments')
313 group.add_argument('--boundaries-only', action='store_true',
314 help="""Index only administrative boundaries.""")
315 group.add_argument('--no-boundaries', action='store_true',
316 help="""Index everything except administrative boundaries.""")
317 group.add_argument('--minrank', '-r', type=int, metavar='RANK', default=0,
318 help='Minimum/starting rank')
319 group.add_argument('--maxrank', '-R', type=int, metavar='RANK', default=30,
320 help='Maximum/finishing rank')
324 from .indexer.indexer import Indexer
326 indexer = Indexer(args.config.get_libpq_dsn(),
327 args.threads or _num_system_cpus() or 1)
329 if not args.no_boundaries:
330 indexer.index_boundaries(args.minrank, args.maxrank)
331 if not args.boundaries_only:
332 indexer.index_by_rank(args.minrank, args.maxrank)
334 if not args.no_boundaries and not args.boundaries_only:
335 indexer.update_status_table()
342 Recompute auxiliary data used by the indexing process.
344 These functions must not be run in parallel with other update commands.
348 def add_args(parser):
349 group = parser.add_argument_group('Data arguments')
350 group.add_argument('--postcodes', action='store_true',
351 help='Update postcode centroid table')
352 group.add_argument('--word-counts', action='store_true',
353 help='Compute frequency of full-word search terms')
354 group.add_argument('--address-levels', action='store_true',
355 help='Reimport address level configuration')
356 group.add_argument('--functions', action='store_true',
357 help='Update the PL/pgSQL functions in the database')
358 group.add_argument('--wiki-data', action='store_true',
359 help='Update Wikipedia/data importance numbers.')
360 group.add_argument('--importance', action='store_true',
361 help='Recompute place importances (expensive!)')
362 group.add_argument('--website', action='store_true',
363 help='Refresh the directory that serves the scripts for the web API')
364 group = parser.add_argument_group('Arguments for function refresh')
365 group.add_argument('--no-diff-updates', action='store_false', dest='diffs',
366 help='Do not enable code for propagating updates')
367 group.add_argument('--enable-debug-statements', action='store_true',
368 help='Enable debug warning statements in functions')
372 from .tools import refresh
374 conn = connect(args.config.get_libpq_dsn())
377 LOG.warning("Update postcodes centroid")
378 refresh.update_postcodes(conn, args.data_dir)
381 LOG.warning('Recompute frequency of full-word search terms')
382 refresh.recompute_word_counts(conn, args.data_dir)
384 if args.address_levels:
385 cfg = Path(args.config.ADDRESS_LEVEL_CONFIG)
386 LOG.warning('Updating address levels from %s', cfg)
387 refresh.load_address_levels_from_file(conn, cfg)
390 LOG.warning('Create functions')
391 refresh.create_functions(conn, args.config, args.data_dir,
392 args.diffs, args.enable_debug_statements)
395 run_legacy_script('setup.php', '--import-wikipedia-articles',
396 nominatim_env=args, throw_on_fail=True)
397 # Attention: importance MUST come after wiki data import.
399 run_legacy_script('update.php', '--recompute-importance',
400 nominatim_env=args, throw_on_fail=True)
402 run_legacy_script('setup.php', '--setup-website',
403 nominatim_env=args, throw_on_fail=True)
410 class AdminCheckDatabase:
412 Check that the database is complete and operational.
416 def add_args(parser):
421 return run_legacy_script('check_import_finished.php', nominatim_env=args)
426 Warm database caches for search and reverse queries.
430 def add_args(parser):
431 group = parser.add_argument_group('Target arguments')
432 group.add_argument('--search-only', action='store_const', dest='target',
434 help="Only pre-warm tables for search queries")
435 group.add_argument('--reverse-only', action='store_const', dest='target',
437 help="Only pre-warm tables for reverse queries")
441 params = ['warm.php']
442 if args.target == 'reverse':
443 params.append('--reverse-only')
444 if args.target == 'search':
445 params.append('--search-only')
446 return run_legacy_script(*params, nominatim_env=args)
451 Export addresses as CSV file from the database.
455 def add_args(parser):
456 group = parser.add_argument_group('Output arguments')
457 group.add_argument('--output-type', default='street',
458 choices=('continent', 'country', 'state', 'county',
459 'city', 'suburb', 'street', 'path'),
460 help='Type of places to output (default: street)')
461 group.add_argument('--output-format',
462 default='street;suburb;city;county;state;country',
463 help="""Semicolon-separated list of address types
464 (see --output-type). Multiple ranks can be
465 merged into one column by simply using a
466 comma-separated list.""")
467 group.add_argument('--output-all-postcodes', action='store_true',
468 help="""List all postcodes for address instead of
469 just the most likely one""")
470 group.add_argument('--language',
471 help="""Preferred language for output
472 (use local name, if omitted)""")
473 group = parser.add_argument_group('Filter arguments')
474 group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
475 help='Export only objects within country')
476 group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
477 help='Export only children of this OSM node')
478 group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
479 help='Export only children of this OSM way')
480 group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
481 help='Export only children of this OSM relation')
486 params = ['export.php',
487 '--output-type', args.output_type,
488 '--output-format', args.output_format]
489 if args.output_all_postcodes:
490 params.append('--output-all-postcodes')
492 params.extend(('--language', args.language))
493 if args.restrict_to_country:
494 params.extend(('--restrict-to-country', args.restrict_to_country))
495 if args.restrict_to_osm_node:
496 params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
497 if args.restrict_to_osm_way:
498 params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
499 if args.restrict_to_osm_relation:
500 params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
502 return run_legacy_script(*params, nominatim_env=args)
505 ('street', 'housenumber and street'),
506 ('city', 'city, town or village'),
507 ('county', 'county'),
509 ('country', 'country'),
510 ('postalcode', 'postcode')
514 ('addressdetails', 'Include a breakdown of the address into elements.'),
515 ('extratags', """Include additional information if available
516 (e.g. wikipedia link, opening hours)."""),
517 ('namedetails', 'Include a list of alternative names.')
521 ('addressdetails', 'Include a breakdown of the address into elements.'),
522 ('keywords', 'Include a list of name keywords and address keywords.'),
523 ('linkedplaces', 'Include a details of places that are linked with this one.'),
524 ('hierarchy', 'Include details of places lower in the address hierarchy.'),
525 ('group_hierarchy', 'Group the places by type.'),
526 ('polygon_geojson', 'Include geometry of result.')
529 def _add_api_output_arguments(parser):
530 group = parser.add_argument_group('Output arguments')
531 group.add_argument('--format', default='jsonv2',
532 choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
533 help='Format of result')
534 for name, desc in EXTRADATA_PARAMS:
535 group.add_argument('--' + name, action='store_true', help=desc)
537 group.add_argument('--lang', '--accept-language', metavar='LANGS',
538 help='Preferred language order for presenting search results')
539 group.add_argument('--polygon-output',
540 choices=['geojson', 'kml', 'svg', 'text'],
541 help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
542 group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
543 help="""Simplify output geometry.
544 Parameter is difference tolerance in degrees.""")
549 Execute API search query.
553 def add_args(parser):
554 group = parser.add_argument_group('Query arguments')
555 group.add_argument('--query',
556 help='Free-form query string')
557 for name, desc in STRUCTURED_QUERY:
558 group.add_argument('--' + name, help='Structured query: ' + desc)
560 _add_api_output_arguments(parser)
562 group = parser.add_argument_group('Result limitation')
563 group.add_argument('--countrycodes', metavar='CC,..',
564 help='Limit search results to one or more countries.')
565 group.add_argument('--exclude_place_ids', metavar='ID,..',
566 help='List of search object to be excluded')
567 group.add_argument('--limit', type=int,
568 help='Limit the number of returned results')
569 group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
570 help='Preferred area to find search results')
571 group.add_argument('--bounded', action='store_true',
572 help='Strictly restrict results to viewbox area')
574 group = parser.add_argument_group('Other arguments')
575 group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
576 help='Do not remove duplicates from the result list')
582 params = dict(q=args.query)
584 params = {k : getattr(args, k) for k, _ in STRUCTURED_QUERY if getattr(args, k)}
586 for param, _ in EXTRADATA_PARAMS:
587 if getattr(args, param):
589 for param in ('format', 'countrycodes', 'exclude_place_ids', 'limit', 'viewbox'):
590 if getattr(args, param):
591 params[param] = getattr(args, param)
593 params['accept-language'] = args.lang
594 if args.polygon_output:
595 params['polygon_' + args.polygon_output] = '1'
596 if args.polygon_threshold:
597 params['polygon_threshold'] = args.polygon_threshold
599 params['bounded'] = '1'
601 params['dedupe'] = '0'
603 return run_api_script('search', args.project_dir,
604 phpcgi_bin=args.phpcgi_path, params=params)
608 Execute API reverse query.
612 def add_args(parser):
613 group = parser.add_argument_group('Query arguments')
614 group.add_argument('--lat', type=float, required=True,
615 help='Latitude of coordinate to look up (in WGS84)')
616 group.add_argument('--lon', type=float, required=True,
617 help='Longitude of coordinate to look up (in WGS84)')
618 group.add_argument('--zoom', type=int,
619 help='Level of detail required for the address')
621 _add_api_output_arguments(parser)
626 params = dict(lat=args.lat, lon=args.lon)
627 if args.zoom is not None:
628 params['zoom'] = args.zoom
630 for param, _ in EXTRADATA_PARAMS:
631 if getattr(args, param):
634 params['format'] = args.format
636 params['accept-language'] = args.lang
637 if args.polygon_output:
638 params['polygon_' + args.polygon_output] = '1'
639 if args.polygon_threshold:
640 params['polygon_threshold'] = args.polygon_threshold
642 return run_api_script('reverse', args.project_dir,
643 phpcgi_bin=args.phpcgi_path, params=params)
648 Execute API reverse query.
652 def add_args(parser):
653 group = parser.add_argument_group('Query arguments')
654 group.add_argument('--id', metavar='OSMID',
655 action='append', required=True, dest='ids',
656 help='OSM id to lookup in format <NRW><id> (may be repeated)')
658 _add_api_output_arguments(parser)
663 params = dict(osm_ids=','.join(args.ids))
665 for param, _ in EXTRADATA_PARAMS:
666 if getattr(args, param):
669 params['format'] = args.format
671 params['accept-language'] = args.lang
672 if args.polygon_output:
673 params['polygon_' + args.polygon_output] = '1'
674 if args.polygon_threshold:
675 params['polygon_threshold'] = args.polygon_threshold
677 return run_api_script('lookup', args.project_dir,
678 phpcgi_bin=args.phpcgi_path, params=params)
683 Execute API lookup query.
687 def add_args(parser):
688 group = parser.add_argument_group('Query arguments')
689 objs = group.add_mutually_exclusive_group(required=True)
690 objs.add_argument('--node', '-n', type=int,
691 help="Look up the OSM node with the given ID.")
692 objs.add_argument('--way', '-w', type=int,
693 help="Look up the OSM way with the given ID.")
694 objs.add_argument('--relation', '-r', type=int,
695 help="Look up the OSM relation with the given ID.")
696 objs.add_argument('--place_id', '-p', type=int,
697 help='Database internal identifier of the OSM object to look up.')
698 group.add_argument('--class', dest='object_class',
699 help="""Class type to disambiguated multiple entries
700 of the same object.""")
702 group = parser.add_argument_group('Output arguments')
703 for name, desc in DETAILS_SWITCHES:
704 group.add_argument('--' + name, action='store_true', help=desc)
705 group.add_argument('--lang', '--accept-language', metavar='LANGS',
706 help='Preferred language order for presenting search results')
711 params = dict(osmtype='N', osmid=args.node)
713 params = dict(osmtype='W', osmid=args.node)
715 params = dict(osmtype='R', osmid=args.node)
717 params = dict(place_id=args.place_id)
718 if args.object_class:
719 params['class'] = args.object_class
720 for name, _ in DETAILS_SWITCHES:
721 params[name] = '1' if getattr(args, name) else '0'
723 return run_api_script('details', args.project_dir,
724 phpcgi_bin=args.phpcgi_path, params=params)
729 Execute API status query.
733 def add_args(parser):
734 group = parser.add_argument_group('API parameters')
735 group.add_argument('--format', default='text', choices=['text', 'json'],
736 help='Format of result')
740 return run_api_script('status', args.project_dir,
741 phpcgi_bin=args.phpcgi_path,
742 params=dict(format=args.format))
745 def nominatim(**kwargs):
747 Command-line tools for importing, updating, administrating and
748 querying the Nominatim database.
750 parser = CommandlineParser('nominatim', nominatim.__doc__)
752 parser.add_subcommand('import', SetupAll)
753 parser.add_subcommand('freeze', SetupFreeze)
754 parser.add_subcommand('replication', UpdateReplication)
756 parser.add_subcommand('check-database', AdminCheckDatabase)
757 parser.add_subcommand('warm', AdminWarm)
759 parser.add_subcommand('special-phrases', SetupSpecialPhrases)
761 parser.add_subcommand('add-data', UpdateAddData)
762 parser.add_subcommand('index', UpdateIndex)
763 parser.add_subcommand('refresh', UpdateRefresh)
765 parser.add_subcommand('export', QueryExport)
767 if kwargs.get('phpcgi_path'):
768 parser.add_subcommand('search', APISearch)
769 parser.add_subcommand('reverse', APIReverse)
770 parser.add_subcommand('lookup', APILookup)
771 parser.add_subcommand('details', APIDetails)
772 parser.add_subcommand('status', APIStatus)
774 parser.parser.epilog = 'php-cgi not found. Query commands not available.'
776 return parser.run(**kwargs)