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""")
238 import osmium # pylint: disable=W0611
239 except ModuleNotFoundError:
240 LOG.fatal("pyosmium not installed. Replication functions not available.\n"
241 "To install pyosmium via pip: pip3 install osmium")
244 from .tools import replication, refresh
246 conn = connect(args.config.get_libpq_dsn())
248 params = ['update.php']
250 LOG.warning("Initialising replication updates")
251 replication.init_replication(conn, args.config.REPLICATION_URL)
252 if args.update_functions:
253 LOG.warning("Create functions")
254 refresh.create_functions(conn, args.config, args.data_dir,
259 if args.check_for_updates:
260 ret = replication.check_for_updates(conn, args.config.REPLICATION_URL)
265 params.append('--import-osmosis')
267 params.append('--import-osmosis-all')
268 if not args.do_index:
269 params.append('--no-index')
271 return run_legacy_script(*params, nominatim_env=args)
276 Add additional data from a file or an online source.
278 Data is only imported, not indexed. You need to call `nominatim-update index`
279 to complete the process.
283 def add_args(parser):
284 group_name = parser.add_argument_group('Source')
285 group = group_name.add_mutually_exclusive_group(required=True)
286 group.add_argument('--file', metavar='FILE',
287 help='Import data from an OSM file')
288 group.add_argument('--diff', metavar='FILE',
289 help='Import data from an OSM diff file')
290 group.add_argument('--node', metavar='ID', type=int,
291 help='Import a single node from the API')
292 group.add_argument('--way', metavar='ID', type=int,
293 help='Import a single way from the API')
294 group.add_argument('--relation', metavar='ID', type=int,
295 help='Import a single relation from the API')
296 group.add_argument('--tiger-data', metavar='DIR',
297 help='Add housenumbers from the US TIGER census database.')
298 group = parser.add_argument_group('Extra arguments')
299 group.add_argument('--use-main-api', action='store_true',
300 help='Use OSM API instead of Overpass to download objects')
305 os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
306 return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
308 params = ['update.php']
310 params.extend(('--import-file', args.file))
312 params.extend(('--import-diff', args.diff))
314 params.extend(('--import-node', args.node))
316 params.extend(('--import-way', args.way))
318 params.extend(('--import-relation', args.relation))
319 if args.use_main_api:
320 params.append('--use-main-api')
321 return run_legacy_script(*params, nominatim_env=args)
326 Reindex all new and modified data.
330 def add_args(parser):
331 group = parser.add_argument_group('Filter arguments')
332 group.add_argument('--boundaries-only', action='store_true',
333 help="""Index only administrative boundaries.""")
334 group.add_argument('--no-boundaries', action='store_true',
335 help="""Index everything except administrative boundaries.""")
336 group.add_argument('--minrank', '-r', type=int, metavar='RANK', default=0,
337 help='Minimum/starting rank')
338 group.add_argument('--maxrank', '-R', type=int, metavar='RANK', default=30,
339 help='Maximum/finishing rank')
343 from .indexer.indexer import Indexer
345 indexer = Indexer(args.config.get_libpq_dsn(),
346 args.threads or _num_system_cpus() or 1)
348 if not args.no_boundaries:
349 indexer.index_boundaries(args.minrank, args.maxrank)
350 if not args.boundaries_only:
351 indexer.index_by_rank(args.minrank, args.maxrank)
353 if not args.no_boundaries and not args.boundaries_only:
354 indexer.update_status_table()
361 Recompute auxiliary data used by the indexing process.
363 These functions must not be run in parallel with other update commands.
367 def add_args(parser):
368 group = parser.add_argument_group('Data arguments')
369 group.add_argument('--postcodes', action='store_true',
370 help='Update postcode centroid table')
371 group.add_argument('--word-counts', action='store_true',
372 help='Compute frequency of full-word search terms')
373 group.add_argument('--address-levels', action='store_true',
374 help='Reimport address level configuration')
375 group.add_argument('--functions', action='store_true',
376 help='Update the PL/pgSQL functions in the database')
377 group.add_argument('--wiki-data', action='store_true',
378 help='Update Wikipedia/data importance numbers.')
379 group.add_argument('--importance', action='store_true',
380 help='Recompute place importances (expensive!)')
381 group.add_argument('--website', action='store_true',
382 help='Refresh the directory that serves the scripts for the web API')
383 group = parser.add_argument_group('Arguments for function refresh')
384 group.add_argument('--no-diff-updates', action='store_false', dest='diffs',
385 help='Do not enable code for propagating updates')
386 group.add_argument('--enable-debug-statements', action='store_true',
387 help='Enable debug warning statements in functions')
391 from .tools import refresh
393 conn = connect(args.config.get_libpq_dsn())
396 LOG.warning("Update postcodes centroid")
397 refresh.update_postcodes(conn, args.data_dir)
400 LOG.warning('Recompute frequency of full-word search terms')
401 refresh.recompute_word_counts(conn, args.data_dir)
403 if args.address_levels:
404 cfg = Path(args.config.ADDRESS_LEVEL_CONFIG)
405 LOG.warning('Updating address levels from %s', cfg)
406 refresh.load_address_levels_from_file(conn, cfg)
409 LOG.warning('Create functions')
410 refresh.create_functions(conn, args.config, args.data_dir,
411 args.diffs, args.enable_debug_statements)
414 run_legacy_script('setup.php', '--import-wikipedia-articles',
415 nominatim_env=args, throw_on_fail=True)
416 # Attention: importance MUST come after wiki data import.
418 run_legacy_script('update.php', '--recompute-importance',
419 nominatim_env=args, throw_on_fail=True)
421 run_legacy_script('setup.php', '--setup-website',
422 nominatim_env=args, throw_on_fail=True)
429 class AdminCheckDatabase:
431 Check that the database is complete and operational.
435 def add_args(parser):
440 return run_legacy_script('check_import_finished.php', nominatim_env=args)
445 Warm database caches for search and reverse queries.
449 def add_args(parser):
450 group = parser.add_argument_group('Target arguments')
451 group.add_argument('--search-only', action='store_const', dest='target',
453 help="Only pre-warm tables for search queries")
454 group.add_argument('--reverse-only', action='store_const', dest='target',
456 help="Only pre-warm tables for reverse queries")
460 params = ['warm.php']
461 if args.target == 'reverse':
462 params.append('--reverse-only')
463 if args.target == 'search':
464 params.append('--search-only')
465 return run_legacy_script(*params, nominatim_env=args)
470 Export addresses as CSV file from the database.
474 def add_args(parser):
475 group = parser.add_argument_group('Output arguments')
476 group.add_argument('--output-type', default='street',
477 choices=('continent', 'country', 'state', 'county',
478 'city', 'suburb', 'street', 'path'),
479 help='Type of places to output (default: street)')
480 group.add_argument('--output-format',
481 default='street;suburb;city;county;state;country',
482 help="""Semicolon-separated list of address types
483 (see --output-type). Multiple ranks can be
484 merged into one column by simply using a
485 comma-separated list.""")
486 group.add_argument('--output-all-postcodes', action='store_true',
487 help="""List all postcodes for address instead of
488 just the most likely one""")
489 group.add_argument('--language',
490 help="""Preferred language for output
491 (use local name, if omitted)""")
492 group = parser.add_argument_group('Filter arguments')
493 group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
494 help='Export only objects within country')
495 group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
496 help='Export only children of this OSM node')
497 group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
498 help='Export only children of this OSM way')
499 group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
500 help='Export only children of this OSM relation')
505 params = ['export.php',
506 '--output-type', args.output_type,
507 '--output-format', args.output_format]
508 if args.output_all_postcodes:
509 params.append('--output-all-postcodes')
511 params.extend(('--language', args.language))
512 if args.restrict_to_country:
513 params.extend(('--restrict-to-country', args.restrict_to_country))
514 if args.restrict_to_osm_node:
515 params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
516 if args.restrict_to_osm_way:
517 params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
518 if args.restrict_to_osm_relation:
519 params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
521 return run_legacy_script(*params, nominatim_env=args)
524 ('street', 'housenumber and street'),
525 ('city', 'city, town or village'),
526 ('county', 'county'),
528 ('country', 'country'),
529 ('postalcode', 'postcode')
533 ('addressdetails', 'Include a breakdown of the address into elements.'),
534 ('extratags', """Include additional information if available
535 (e.g. wikipedia link, opening hours)."""),
536 ('namedetails', 'Include a list of alternative names.')
540 ('addressdetails', 'Include a breakdown of the address into elements.'),
541 ('keywords', 'Include a list of name keywords and address keywords.'),
542 ('linkedplaces', 'Include a details of places that are linked with this one.'),
543 ('hierarchy', 'Include details of places lower in the address hierarchy.'),
544 ('group_hierarchy', 'Group the places by type.'),
545 ('polygon_geojson', 'Include geometry of result.')
548 def _add_api_output_arguments(parser):
549 group = parser.add_argument_group('Output arguments')
550 group.add_argument('--format', default='jsonv2',
551 choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
552 help='Format of result')
553 for name, desc in EXTRADATA_PARAMS:
554 group.add_argument('--' + name, action='store_true', help=desc)
556 group.add_argument('--lang', '--accept-language', metavar='LANGS',
557 help='Preferred language order for presenting search results')
558 group.add_argument('--polygon-output',
559 choices=['geojson', 'kml', 'svg', 'text'],
560 help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
561 group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
562 help="""Simplify output geometry.
563 Parameter is difference tolerance in degrees.""")
568 Execute API search query.
572 def add_args(parser):
573 group = parser.add_argument_group('Query arguments')
574 group.add_argument('--query',
575 help='Free-form query string')
576 for name, desc in STRUCTURED_QUERY:
577 group.add_argument('--' + name, help='Structured query: ' + desc)
579 _add_api_output_arguments(parser)
581 group = parser.add_argument_group('Result limitation')
582 group.add_argument('--countrycodes', metavar='CC,..',
583 help='Limit search results to one or more countries.')
584 group.add_argument('--exclude_place_ids', metavar='ID,..',
585 help='List of search object to be excluded')
586 group.add_argument('--limit', type=int,
587 help='Limit the number of returned results')
588 group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
589 help='Preferred area to find search results')
590 group.add_argument('--bounded', action='store_true',
591 help='Strictly restrict results to viewbox area')
593 group = parser.add_argument_group('Other arguments')
594 group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
595 help='Do not remove duplicates from the result list')
601 params = dict(q=args.query)
603 params = {k : getattr(args, k) for k, _ in STRUCTURED_QUERY if getattr(args, k)}
605 for param, _ in EXTRADATA_PARAMS:
606 if getattr(args, param):
608 for param in ('format', 'countrycodes', 'exclude_place_ids', 'limit', 'viewbox'):
609 if getattr(args, param):
610 params[param] = getattr(args, param)
612 params['accept-language'] = args.lang
613 if args.polygon_output:
614 params['polygon_' + args.polygon_output] = '1'
615 if args.polygon_threshold:
616 params['polygon_threshold'] = args.polygon_threshold
618 params['bounded'] = '1'
620 params['dedupe'] = '0'
622 return run_api_script('search', args.project_dir,
623 phpcgi_bin=args.phpcgi_path, params=params)
627 Execute API reverse query.
631 def add_args(parser):
632 group = parser.add_argument_group('Query arguments')
633 group.add_argument('--lat', type=float, required=True,
634 help='Latitude of coordinate to look up (in WGS84)')
635 group.add_argument('--lon', type=float, required=True,
636 help='Longitude of coordinate to look up (in WGS84)')
637 group.add_argument('--zoom', type=int,
638 help='Level of detail required for the address')
640 _add_api_output_arguments(parser)
645 params = dict(lat=args.lat, lon=args.lon)
646 if args.zoom is not None:
647 params['zoom'] = args.zoom
649 for param, _ in EXTRADATA_PARAMS:
650 if getattr(args, param):
653 params['format'] = args.format
655 params['accept-language'] = args.lang
656 if args.polygon_output:
657 params['polygon_' + args.polygon_output] = '1'
658 if args.polygon_threshold:
659 params['polygon_threshold'] = args.polygon_threshold
661 return run_api_script('reverse', args.project_dir,
662 phpcgi_bin=args.phpcgi_path, params=params)
667 Execute API reverse query.
671 def add_args(parser):
672 group = parser.add_argument_group('Query arguments')
673 group.add_argument('--id', metavar='OSMID',
674 action='append', required=True, dest='ids',
675 help='OSM id to lookup in format <NRW><id> (may be repeated)')
677 _add_api_output_arguments(parser)
682 params = dict(osm_ids=','.join(args.ids))
684 for param, _ in EXTRADATA_PARAMS:
685 if getattr(args, param):
688 params['format'] = args.format
690 params['accept-language'] = args.lang
691 if args.polygon_output:
692 params['polygon_' + args.polygon_output] = '1'
693 if args.polygon_threshold:
694 params['polygon_threshold'] = args.polygon_threshold
696 return run_api_script('lookup', args.project_dir,
697 phpcgi_bin=args.phpcgi_path, params=params)
702 Execute API lookup query.
706 def add_args(parser):
707 group = parser.add_argument_group('Query arguments')
708 objs = group.add_mutually_exclusive_group(required=True)
709 objs.add_argument('--node', '-n', type=int,
710 help="Look up the OSM node with the given ID.")
711 objs.add_argument('--way', '-w', type=int,
712 help="Look up the OSM way with the given ID.")
713 objs.add_argument('--relation', '-r', type=int,
714 help="Look up the OSM relation with the given ID.")
715 objs.add_argument('--place_id', '-p', type=int,
716 help='Database internal identifier of the OSM object to look up.')
717 group.add_argument('--class', dest='object_class',
718 help="""Class type to disambiguated multiple entries
719 of the same object.""")
721 group = parser.add_argument_group('Output arguments')
722 for name, desc in DETAILS_SWITCHES:
723 group.add_argument('--' + name, action='store_true', help=desc)
724 group.add_argument('--lang', '--accept-language', metavar='LANGS',
725 help='Preferred language order for presenting search results')
730 params = dict(osmtype='N', osmid=args.node)
732 params = dict(osmtype='W', osmid=args.node)
734 params = dict(osmtype='R', osmid=args.node)
736 params = dict(place_id=args.place_id)
737 if args.object_class:
738 params['class'] = args.object_class
739 for name, _ in DETAILS_SWITCHES:
740 params[name] = '1' if getattr(args, name) else '0'
742 return run_api_script('details', args.project_dir,
743 phpcgi_bin=args.phpcgi_path, params=params)
748 Execute API status query.
752 def add_args(parser):
753 group = parser.add_argument_group('API parameters')
754 group.add_argument('--format', default='text', choices=['text', 'json'],
755 help='Format of result')
759 return run_api_script('status', args.project_dir,
760 phpcgi_bin=args.phpcgi_path,
761 params=dict(format=args.format))
764 def nominatim(**kwargs):
766 Command-line tools for importing, updating, administrating and
767 querying the Nominatim database.
769 parser = CommandlineParser('nominatim', nominatim.__doc__)
771 parser.add_subcommand('import', SetupAll)
772 parser.add_subcommand('freeze', SetupFreeze)
773 parser.add_subcommand('replication', UpdateReplication)
775 parser.add_subcommand('check-database', AdminCheckDatabase)
776 parser.add_subcommand('warm', AdminWarm)
778 parser.add_subcommand('special-phrases', SetupSpecialPhrases)
780 parser.add_subcommand('add-data', UpdateAddData)
781 parser.add_subcommand('index', UpdateIndex)
782 parser.add_subcommand('refresh', UpdateRefresh)
784 parser.add_subcommand('export', QueryExport)
786 if kwargs.get('phpcgi_path'):
787 parser.add_subcommand('search', APISearch)
788 parser.add_subcommand('reverse', APIReverse)
789 parser.add_subcommand('lookup', APILookup)
790 parser.add_subcommand('details', APIDetails)
791 parser.add_subcommand('status', APIStatus)
793 parser.parser.epilog = 'php-cgi not found. Query commands not available.'
795 return parser.run(**kwargs)