2 Command-line interface to the Nominatim functions for import, update,
3 database administration and querying.
9 from pathlib import Path
13 from .config import Configuration
14 from .tools.exec_utils import run_legacy_script, run_api_script
16 LOG = logging.getLogger()
18 def _num_system_cpus():
20 cpus = len(os.sched_getaffinity(0))
21 except NotImplementedError:
24 return cpus or os.cpu_count()
27 class CommandlineParser:
28 """ Wraps some of the common functions for parsing the command line
29 and setting up subcommands.
31 def __init__(self, prog, description):
32 self.parser = argparse.ArgumentParser(
34 description=description,
35 formatter_class=argparse.RawDescriptionHelpFormatter)
37 self.subs = self.parser.add_subparsers(title='available commands',
40 # Arguments added to every sub-command
41 self.default_args = argparse.ArgumentParser(add_help=False)
42 group = self.default_args.add_argument_group('Default arguments')
43 group.add_argument('-h', '--help', action='help',
44 help='Show this help message and exit')
45 group.add_argument('-q', '--quiet', action='store_const', const=0,
46 dest='verbose', default=1,
47 help='Print only error messages')
48 group.add_argument('-v', '--verbose', action='count', default=1,
49 help='Increase verboseness of output')
50 group.add_argument('--project-dir', metavar='DIR', default='.',
51 help='Base directory of the Nominatim installation (default:.)')
52 group.add_argument('-j', '--threads', metavar='NUM', type=int,
53 help='Number of parallel threads to use')
56 def add_subcommand(self, name, cmd):
57 """ Add a subcommand to the parser. The subcommand must be a class
58 with a function add_args() that adds the parameters for the
59 subcommand and a run() function that executes the command.
61 parser = self.subs.add_parser(name, parents=[self.default_args],
62 help=cmd.__doc__.split('\n', 1)[0],
63 description=cmd.__doc__,
64 formatter_class=argparse.RawDescriptionHelpFormatter,
66 parser.set_defaults(command=cmd)
69 def run(self, **kwargs):
70 """ Parse the command line arguments of the program and execute the
71 appropriate subcommand.
73 args = self.parser.parse_args(args=kwargs.get('cli_args'))
75 if args.subcommand is None:
76 self.parser.print_help()
79 for arg in ('module_dir', 'osm2pgsql_path', 'phplib_dir', 'data_dir', 'phpcgi_path'):
80 setattr(args, arg, Path(kwargs[arg]))
81 args.project_dir = Path(args.project_dir)
83 logging.basicConfig(stream=sys.stderr,
84 format='%(asctime)s: %(message)s',
85 datefmt='%Y-%m-%d %H:%M:%S',
86 level=max(4 - args.verbose, 1) * 10)
88 args.config = Configuration(args.project_dir, args.data_dir / 'settings')
90 return args.command.run(args)
92 ##### Subcommand classes
94 # Each class needs to implement two functions: add_args() adds the CLI parameters
95 # for the subfunction, run() executes the subcommand.
97 # The class documentation doubles as the help text for the command. The
98 # first line is also used in the summary when calling the program without
101 # No need to document the functions each time.
102 # pylint: disable=C0111
107 Create a new Nominatim database from an OSM file.
111 def add_args(parser):
112 group_name = parser.add_argument_group('Required arguments')
113 group = group_name.add_mutually_exclusive_group(required=True)
114 group.add_argument('--osm-file',
115 help='OSM file to be imported.')
116 group.add_argument('--continue', dest='continue_at',
117 choices=['load-data', 'indexing', 'db-postprocess'],
118 help='Continue an import that was interrupted')
119 group = parser.add_argument_group('Optional arguments')
120 group.add_argument('--osm2pgsql-cache', metavar='SIZE', type=int,
121 help='Size of cache to be used by osm2pgsql (in MB)')
122 group.add_argument('--reverse-only', action='store_true',
123 help='Do not create tables and indexes for searching')
124 group.add_argument('--enable-debug-statements', action='store_true',
125 help='Include debug warning statements in SQL code')
126 group.add_argument('--no-partitions', action='store_true',
127 help="""Do not partition search indices
128 (speeds up import of single country extracts)""")
129 group.add_argument('--no-updates', action='store_true',
130 help="""Do not keep tables that are only needed for
131 updating the database later""")
132 group = parser.add_argument_group('Expert options')
133 group.add_argument('--ignore-errors', action='store_true',
134 help='Continue import even when errors in SQL are present')
135 group.add_argument('--index-noanalyse', action='store_true',
136 help='Do not perform analyse operations during index')
141 params = ['setup.php']
143 params.extend(('--all', '--osm-file', args.osm_file))
145 if args.continue_at == 'load-data':
146 params.append('--load-data')
147 if args.continue_at in ('load-data', 'indexing'):
148 params.append('--index')
149 params.extend(('--create-search-indices', '--create-country-names',
151 if args.osm2pgsql_cache:
152 params.extend(('--osm2pgsql-cache', args.osm2pgsql_cache))
153 if args.reverse_only:
154 params.append('--reverse-only')
155 if args.enable_debug_statements:
156 params.append('--enable-debug-statements')
157 if args.no_partitions:
158 params.append('--no-partitions')
160 params.append('--drop')
161 if args.ignore_errors:
162 params.append('--ignore-errors')
163 if args.index_noanalyse:
164 params.append('--index-noanalyse')
166 return run_legacy_script(*params, nominatim_env=args)
171 Make database read-only.
173 About half of data in the Nominatim database is kept only to be able to
174 keep the data up-to-date with new changes made in OpenStreetMap. This
175 command drops all this data and only keeps the part needed for geocoding
178 This command has the same effect as the `--no-updates` option for imports.
182 def add_args(parser):
187 return run_legacy_script('setup.php', '--drop', nominatim_env=args)
190 class SetupSpecialPhrases:
192 Maintain special phrases.
196 def add_args(parser):
197 group = parser.add_argument_group('Input arguments')
198 group.add_argument('--from-wiki', action='store_true',
199 help='Pull special phrases from the OSM wiki.')
200 group = parser.add_argument_group('Output arguments')
201 group.add_argument('-o', '--output', default='-',
202 help="""File to write the preprocessed phrases to.
203 If omitted, it will be written to stdout.""")
207 if args.output != '-':
208 raise NotImplementedError('Only output to stdout is currently implemented.')
209 return run_legacy_script('specialphrases.php', '--wiki-import', nominatim_env=args)
212 class UpdateReplication:
214 Update the database using an online replication service.
218 def add_args(parser):
219 group = parser.add_argument_group('Arguments for initialisation')
220 group.add_argument('--init', action='store_true',
221 help='Initialise the update process')
222 group.add_argument('--no-update-functions', dest='update_functions',
223 action='store_false',
224 help="""Do not update the trigger function to
225 support differential updates.""")
226 group = parser.add_argument_group('Arguments for updates')
227 group.add_argument('--check-for-updates', action='store_true',
228 help='Check if new updates are available and exit')
229 group.add_argument('--once', action='store_true',
230 help="""Download and apply updates only once. When
231 not set, updates are continuously applied""")
232 group.add_argument('--no-index', action='store_false', dest='do_index',
233 help="""Do not index the new data. Only applicable
234 together with --once""")
238 params = ['update.php']
240 params.append('--init-updates')
241 if not args.update_functions:
242 params.append('--no-update-functions')
243 elif args.check_for_updates:
244 params.append('--check-for-updates')
247 params.append('--import-osmosis')
249 params.append('--import-osmosis-all')
250 if not args.do_index:
251 params.append('--no-index')
253 return run_legacy_script(*params, nominatim_env=args)
258 Add additional data from a file or an online source.
260 Data is only imported, not indexed. You need to call `nominatim-update index`
261 to complete the process.
265 def add_args(parser):
266 group_name = parser.add_argument_group('Source')
267 group = group_name.add_mutually_exclusive_group(required=True)
268 group.add_argument('--file', metavar='FILE',
269 help='Import data from an OSM file')
270 group.add_argument('--diff', metavar='FILE',
271 help='Import data from an OSM diff file')
272 group.add_argument('--node', metavar='ID', type=int,
273 help='Import a single node from the API')
274 group.add_argument('--way', metavar='ID', type=int,
275 help='Import a single way from the API')
276 group.add_argument('--relation', metavar='ID', type=int,
277 help='Import a single relation from the API')
278 group.add_argument('--tiger-data', metavar='DIR',
279 help='Add housenumbers from the US TIGER census database.')
280 group = parser.add_argument_group('Extra arguments')
281 group.add_argument('--use-main-api', action='store_true',
282 help='Use OSM API instead of Overpass to download objects')
287 os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
288 return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
290 params = ['update.php']
292 params.extend(('--import-file', args.file))
294 params.extend(('--import-diff', args.diff))
296 params.extend(('--import-node', args.node))
298 params.extend(('--import-way', args.way))
300 params.extend(('--import-relation', args.relation))
301 if args.use_main_api:
302 params.append('--use-main-api')
303 return run_legacy_script(*params, nominatim_env=args)
308 Reindex all new and modified data.
312 def add_args(parser):
313 group = parser.add_argument_group('Filter arguments')
314 group.add_argument('--boundaries-only', action='store_true',
315 help="""Index only administrative boundaries.""")
316 group.add_argument('--no-boundaries', action='store_true',
317 help="""Index everything except administrative boundaries.""")
318 group.add_argument('--minrank', '-r', type=int, metavar='RANK', default=0,
319 help='Minimum/starting rank')
320 group.add_argument('--maxrank', '-R', type=int, metavar='RANK', default=30,
321 help='Maximum/finishing rank')
325 from .indexer.indexer import Indexer
327 indexer = Indexer(args.config.get_libpq_dsn(),
328 args.threads or _num_system_cpus() or 1)
330 if not args.no_boundaries:
331 indexer.index_boundaries(args.minrank, args.maxrank)
332 if not args.boundaries_only:
333 indexer.index_by_rank(args.minrank, args.maxrank)
335 if not args.no_boundaries and not args.boundaries_only:
336 indexer.update_status_table()
343 Recompute auxiliary data used by the indexing process.
345 These functions must not be run in parallel with other update commands.
349 def add_args(parser):
350 group = parser.add_argument_group('Data arguments')
351 group.add_argument('--postcodes', action='store_true',
352 help='Update postcode centroid table')
353 group.add_argument('--word-counts', action='store_true',
354 help='Compute frequency of full-word search terms')
355 group.add_argument('--address-levels', action='store_true',
356 help='Reimport address level configuration')
357 group.add_argument('--functions', action='store_true',
358 help='Update the PL/pgSQL functions in the database')
359 group.add_argument('--wiki-data', action='store_true',
360 help='Update Wikipedia/data importance numbers.')
361 group.add_argument('--importance', action='store_true',
362 help='Recompute place importances (expensive!)')
363 group.add_argument('--website', action='store_true',
364 help='Refresh the directory that serves the scripts for the web API')
365 group = parser.add_argument_group('Arguments for function refresh')
366 group.add_argument('--no-diff-updates', action='store_false', dest='diffs',
367 help='Do not enable code for propagating updates')
368 group.add_argument('--enable-debug-statements', action='store_true',
369 help='Enable debug warning statements in functions')
373 import nominatim.tools.refresh
375 with psycopg2.connect(args.config.get_libpq_dsn()) as conn:
377 LOG.warning("Update postcodes centroid")
378 nominatim.tools.refresh.update_postcodes(conn, args.data_dir)
380 LOG.warning('Recompute frequency of full-word search terms')
381 nominatim.tools.refresh.recompute_word_counts(conn, args.data_dir)
382 if args.address_levels:
383 run_legacy_script('update.php', '--update-address-levels',
384 nominatim_env=args, throw_on_fail=True)
386 params = ['setup.php', '--create-functions', '--create-partition-functions']
388 params.append('--enable-diff-updates')
389 if args.enable_debug_statements:
390 params.append('--enable-debug-statements')
391 run_legacy_script(*params, nominatim_env=args, throw_on_fail=True)
393 run_legacy_script('setup.php', '--import-wikipedia-articles',
394 nominatim_env=args, throw_on_fail=True)
395 # Attention: importance MUST come after wiki data import.
397 run_legacy_script('update.php', '--recompute-importance',
398 nominatim_env=args, throw_on_fail=True)
400 run_legacy_script('setup.php', '--setup-website',
401 nominatim_env=args, throw_on_fail=True)
405 class AdminCheckDatabase:
407 Check that the database is complete and operational.
411 def add_args(parser):
416 return run_legacy_script('check_import_finished.php', nominatim_env=args)
421 Warm database caches for search and reverse queries.
425 def add_args(parser):
426 group = parser.add_argument_group('Target arguments')
427 group.add_argument('--search-only', action='store_const', dest='target',
429 help="Only pre-warm tables for search queries")
430 group.add_argument('--reverse-only', action='store_const', dest='target',
432 help="Only pre-warm tables for reverse queries")
436 params = ['warm.php']
437 if args.target == 'reverse':
438 params.append('--reverse-only')
439 if args.target == 'search':
440 params.append('--search-only')
441 return run_legacy_script(*params, nominatim_env=args)
446 Export addresses as CSV file from the database.
450 def add_args(parser):
451 group = parser.add_argument_group('Output arguments')
452 group.add_argument('--output-type', default='street',
453 choices=('continent', 'country', 'state', 'county',
454 'city', 'suburb', 'street', 'path'),
455 help='Type of places to output (default: street)')
456 group.add_argument('--output-format',
457 default='street;suburb;city;county;state;country',
458 help="""Semicolon-separated list of address types
459 (see --output-type). Multiple ranks can be
460 merged into one column by simply using a
461 comma-separated list.""")
462 group.add_argument('--output-all-postcodes', action='store_true',
463 help="""List all postcodes for address instead of
464 just the most likely one""")
465 group.add_argument('--language',
466 help="""Preferred language for output
467 (use local name, if omitted)""")
468 group = parser.add_argument_group('Filter arguments')
469 group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
470 help='Export only objects within country')
471 group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
472 help='Export only children of this OSM node')
473 group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
474 help='Export only children of this OSM way')
475 group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
476 help='Export only children of this OSM relation')
481 params = ['export.php',
482 '--output-type', args.output_type,
483 '--output-format', args.output_format]
484 if args.output_all_postcodes:
485 params.append('--output-all-postcodes')
487 params.extend(('--language', args.language))
488 if args.restrict_to_country:
489 params.extend(('--restrict-to-country', args.restrict_to_country))
490 if args.restrict_to_osm_node:
491 params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
492 if args.restrict_to_osm_way:
493 params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
494 if args.restrict_to_osm_relation:
495 params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
497 return run_legacy_script(*params, nominatim_env=args)
500 ('street', 'housenumber and street'),
501 ('city', 'city, town or village'),
502 ('county', 'county'),
504 ('country', 'country'),
505 ('postalcode', 'postcode')
509 ('addressdetails', 'Include a breakdown of the address into elements.'),
510 ('extratags', """Include additional information if available
511 (e.g. wikipedia link, opening hours)."""),
512 ('namedetails', 'Include a list of alternative names.')
516 ('addressdetails', 'Include a breakdown of the address into elements.'),
517 ('keywords', 'Include a list of name keywords and address keywords.'),
518 ('linkedplaces', 'Include a details of places that are linked with this one.'),
519 ('hierarchy', 'Include details of places lower in the address hierarchy.'),
520 ('group_hierarchy', 'Group the places by type.'),
521 ('polygon_geojson', 'Include geometry of result.')
524 def _add_api_output_arguments(parser):
525 group = parser.add_argument_group('Output arguments')
526 group.add_argument('--format', default='jsonv2',
527 choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
528 help='Format of result')
529 for name, desc in EXTRADATA_PARAMS:
530 group.add_argument('--' + name, action='store_true', help=desc)
532 group.add_argument('--lang', '--accept-language', metavar='LANGS',
533 help='Preferred language order for presenting search results')
534 group.add_argument('--polygon-output',
535 choices=['geojson', 'kml', 'svg', 'text'],
536 help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
537 group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
538 help="""Simplify output geometry.
539 Parameter is difference tolerance in degrees.""")
544 Execute API search query.
548 def add_args(parser):
549 group = parser.add_argument_group('Query arguments')
550 group.add_argument('--query',
551 help='Free-form query string')
552 for name, desc in STRUCTURED_QUERY:
553 group.add_argument('--' + name, help='Structured query: ' + desc)
555 _add_api_output_arguments(parser)
557 group = parser.add_argument_group('Result limitation')
558 group.add_argument('--countrycodes', metavar='CC,..',
559 help='Limit search results to one or more countries.')
560 group.add_argument('--exclude_place_ids', metavar='ID,..',
561 help='List of search object to be excluded')
562 group.add_argument('--limit', type=int,
563 help='Limit the number of returned results')
564 group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
565 help='Preferred area to find search results')
566 group.add_argument('--bounded', action='store_true',
567 help='Strictly restrict results to viewbox area')
569 group = parser.add_argument_group('Other arguments')
570 group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
571 help='Do not remove duplicates from the result list')
577 params = dict(q=args.query)
579 params = {k : getattr(args, k) for k, _ in STRUCTURED_QUERY if getattr(args, k)}
581 for param, _ in EXTRADATA_PARAMS:
582 if getattr(args, param):
584 for param in ('format', 'countrycodes', 'exclude_place_ids', 'limit', 'viewbox'):
585 if getattr(args, param):
586 params[param] = getattr(args, param)
588 params['accept-language'] = args.lang
589 if args.polygon_output:
590 params['polygon_' + args.polygon_output] = '1'
591 if args.polygon_threshold:
592 params['polygon_threshold'] = args.polygon_threshold
594 params['bounded'] = '1'
596 params['dedupe'] = '0'
598 return run_api_script('search', args.project_dir,
599 phpcgi_bin=args.phpcgi_path, params=params)
603 Execute API reverse query.
607 def add_args(parser):
608 group = parser.add_argument_group('Query arguments')
609 group.add_argument('--lat', type=float, required=True,
610 help='Latitude of coordinate to look up (in WGS84)')
611 group.add_argument('--lon', type=float, required=True,
612 help='Longitude of coordinate to look up (in WGS84)')
613 group.add_argument('--zoom', type=int,
614 help='Level of detail required for the address')
616 _add_api_output_arguments(parser)
621 params = dict(lat=args.lat, lon=args.lon)
622 if args.zoom is not None:
623 params['zoom'] = args.zoom
625 for param, _ in EXTRADATA_PARAMS:
626 if getattr(args, param):
629 params['format'] = args.format
631 params['accept-language'] = args.lang
632 if args.polygon_output:
633 params['polygon_' + args.polygon_output] = '1'
634 if args.polygon_threshold:
635 params['polygon_threshold'] = args.polygon_threshold
637 return run_api_script('reverse', args.project_dir,
638 phpcgi_bin=args.phpcgi_path, params=params)
643 Execute API reverse query.
647 def add_args(parser):
648 group = parser.add_argument_group('Query arguments')
649 group.add_argument('--id', metavar='OSMID',
650 action='append', required=True, dest='ids',
651 help='OSM id to lookup in format <NRW><id> (may be repeated)')
653 _add_api_output_arguments(parser)
658 params = dict(osm_ids=','.join(args.ids))
660 for param, _ in EXTRADATA_PARAMS:
661 if getattr(args, param):
664 params['format'] = args.format
666 params['accept-language'] = args.lang
667 if args.polygon_output:
668 params['polygon_' + args.polygon_output] = '1'
669 if args.polygon_threshold:
670 params['polygon_threshold'] = args.polygon_threshold
672 return run_api_script('lookup', args.project_dir,
673 phpcgi_bin=args.phpcgi_path, params=params)
678 Execute API lookup query.
682 def add_args(parser):
683 group = parser.add_argument_group('Query arguments')
684 objs = group.add_mutually_exclusive_group(required=True)
685 objs.add_argument('--node', '-n', type=int,
686 help="Look up the OSM node with the given ID.")
687 objs.add_argument('--way', '-w', type=int,
688 help="Look up the OSM way with the given ID.")
689 objs.add_argument('--relation', '-r', type=int,
690 help="Look up the OSM relation with the given ID.")
691 objs.add_argument('--place_id', '-p', type=int,
692 help='Database internal identifier of the OSM object to look up.')
693 group.add_argument('--class', dest='object_class',
694 help="""Class type to disambiguated multiple entries
695 of the same object.""")
697 group = parser.add_argument_group('Output arguments')
698 for name, desc in DETAILS_SWITCHES:
699 group.add_argument('--' + name, action='store_true', help=desc)
700 group.add_argument('--lang', '--accept-language', metavar='LANGS',
701 help='Preferred language order for presenting search results')
706 params = dict(osmtype='N', osmid=args.node)
708 params = dict(osmtype='W', osmid=args.node)
710 params = dict(osmtype='R', osmid=args.node)
712 params = dict(place_id=args.place_id)
713 if args.object_class:
714 params['class'] = args.object_class
715 for name, _ in DETAILS_SWITCHES:
716 params[name] = '1' if getattr(args, name) else '0'
718 return run_api_script('details', args.project_dir,
719 phpcgi_bin=args.phpcgi_path, params=params)
724 Execute API status query.
728 def add_args(parser):
729 group = parser.add_argument_group('API parameters')
730 group.add_argument('--format', default='text', choices=['text', 'json'],
731 help='Format of result')
735 return run_api_script('status', args.project_dir,
736 phpcgi_bin=args.phpcgi_path,
737 params=dict(format=args.format))
740 def nominatim(**kwargs):
742 Command-line tools for importing, updating, administrating and
743 querying the Nominatim database.
745 parser = CommandlineParser('nominatim', nominatim.__doc__)
747 parser.add_subcommand('import', SetupAll)
748 parser.add_subcommand('freeze', SetupFreeze)
749 parser.add_subcommand('replication', UpdateReplication)
751 parser.add_subcommand('check-database', AdminCheckDatabase)
752 parser.add_subcommand('warm', AdminWarm)
754 parser.add_subcommand('special-phrases', SetupSpecialPhrases)
756 parser.add_subcommand('add-data', UpdateAddData)
757 parser.add_subcommand('index', UpdateIndex)
758 parser.add_subcommand('refresh', UpdateRefresh)
760 parser.add_subcommand('export', QueryExport)
762 if kwargs.get('phpcgi_path'):
763 parser.add_subcommand('search', APISearch)
764 parser.add_subcommand('reverse', APIReverse)
765 parser.add_subcommand('lookup', APILookup)
766 parser.add_subcommand('details', APIDetails)
767 parser.add_subcommand('status', APIStatus)
769 parser.parser.epilog = 'php-cgi not found. Query commands not available.'
771 return parser.run(**kwargs)