]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/cli.py
port replication initialisation to Python
[nominatim.git] / nominatim / cli.py
1 """
2 Command-line interface to the Nominatim functions for import, update,
3 database administration and querying.
4 """
5 import sys
6 import os
7 import argparse
8 import logging
9 from pathlib import Path
10
11 from .config import Configuration
12 from .tools.exec_utils import run_legacy_script, run_api_script
13 from .db.connection import connect
14
15 LOG = logging.getLogger()
16
17 def _num_system_cpus():
18     try:
19         cpus = len(os.sched_getaffinity(0))
20     except NotImplementedError:
21         cpus = None
22
23     return cpus or os.cpu_count()
24
25
26 class CommandlineParser:
27     """ Wraps some of the common functions for parsing the command line
28         and setting up subcommands.
29     """
30     def __init__(self, prog, description):
31         self.parser = argparse.ArgumentParser(
32             prog=prog,
33             description=description,
34             formatter_class=argparse.RawDescriptionHelpFormatter)
35
36         self.subs = self.parser.add_subparsers(title='available commands',
37                                                dest='subcommand')
38
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')
53
54
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.
59         """
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,
64                                       add_help=False)
65         parser.set_defaults(command=cmd)
66         cmd.add_args(parser)
67
68     def run(self, **kwargs):
69         """ Parse the command line arguments of the program and execute the
70             appropriate subcommand.
71         """
72         args = self.parser.parse_args(args=kwargs.get('cli_args'))
73
74         if args.subcommand is None:
75             self.parser.print_help()
76             return 1
77
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)
81
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)
86
87         args.config = Configuration(args.project_dir, args.data_dir / 'settings')
88
89         return args.command.run(args)
90
91 ##### Subcommand classes
92 #
93 # Each class needs to implement two functions: add_args() adds the CLI parameters
94 # for the subfunction, run() executes the subcommand.
95 #
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
98 # a subcommand.
99 #
100 # No need to document the functions each time.
101 # pylint: disable=C0111
102
103
104 class SetupAll:
105     """\
106     Create a new Nominatim database from an OSM file.
107     """
108
109     @staticmethod
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')
136
137
138     @staticmethod
139     def run(args):
140         params = ['setup.php']
141         if args.osm_file:
142             params.extend(('--all', '--osm-file', args.osm_file))
143         else:
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',
149                            '--setup-website'))
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')
158         if args.no_updates:
159             params.append('--drop')
160         if args.ignore_errors:
161             params.append('--ignore-errors')
162         if args.index_noanalyse:
163             params.append('--index-noanalyse')
164
165         return run_legacy_script(*params, nominatim_env=args)
166
167
168 class SetupFreeze:
169     """\
170     Make database read-only.
171
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
175     itself.
176
177     This command has the same effect as the `--no-updates` option for imports.
178     """
179
180     @staticmethod
181     def add_args(parser):
182         pass # No options
183
184     @staticmethod
185     def run(args):
186         return run_legacy_script('setup.php', '--drop', nominatim_env=args)
187
188
189 class SetupSpecialPhrases:
190     """\
191     Maintain special phrases.
192     """
193
194     @staticmethod
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.""")
203
204     @staticmethod
205     def run(args):
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)
209
210
211 class UpdateReplication:
212     """\
213     Update the database using an online replication service.
214     """
215
216     @staticmethod
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""")
234
235     @staticmethod
236     def run(args):
237         try:
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")
242             return 1
243
244         from .tools import replication, refresh
245
246         conn = connect(args.config.get_libpq_dsn())
247
248         params = ['update.php']
249         if args.init:
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,
255                                          True, False)
256             conn.close()
257             return 0
258
259         if args.check_for_updates:
260             params.append('--check-for-updates')
261         else:
262             if args.once:
263                 params.append('--import-osmosis')
264             else:
265                 params.append('--import-osmosis-all')
266             if not args.do_index:
267                 params.append('--no-index')
268
269         return run_legacy_script(*params, nominatim_env=args)
270
271
272 class UpdateAddData:
273     """\
274     Add additional data from a file or an online source.
275
276     Data is only imported, not indexed. You need to call `nominatim-update index`
277     to complete the process.
278     """
279
280     @staticmethod
281     def add_args(parser):
282         group_name = parser.add_argument_group('Source')
283         group = group_name.add_mutually_exclusive_group(required=True)
284         group.add_argument('--file', metavar='FILE',
285                            help='Import data from an OSM file')
286         group.add_argument('--diff', metavar='FILE',
287                            help='Import data from an OSM diff file')
288         group.add_argument('--node', metavar='ID', type=int,
289                            help='Import a single node from the API')
290         group.add_argument('--way', metavar='ID', type=int,
291                            help='Import a single way from the API')
292         group.add_argument('--relation', metavar='ID', type=int,
293                            help='Import a single relation from the API')
294         group.add_argument('--tiger-data', metavar='DIR',
295                            help='Add housenumbers from the US TIGER census database.')
296         group = parser.add_argument_group('Extra arguments')
297         group.add_argument('--use-main-api', action='store_true',
298                            help='Use OSM API instead of Overpass to download objects')
299
300     @staticmethod
301     def run(args):
302         if args.tiger_data:
303             os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
304             return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
305
306         params = ['update.php']
307         if args.file:
308             params.extend(('--import-file', args.file))
309         elif args.diff:
310             params.extend(('--import-diff', args.diff))
311         elif args.node:
312             params.extend(('--import-node', args.node))
313         elif args.way:
314             params.extend(('--import-way', args.way))
315         elif args.relation:
316             params.extend(('--import-relation', args.relation))
317         if args.use_main_api:
318             params.append('--use-main-api')
319         return run_legacy_script(*params, nominatim_env=args)
320
321
322 class UpdateIndex:
323     """\
324     Reindex all new and modified data.
325     """
326
327     @staticmethod
328     def add_args(parser):
329         group = parser.add_argument_group('Filter arguments')
330         group.add_argument('--boundaries-only', action='store_true',
331                            help="""Index only administrative boundaries.""")
332         group.add_argument('--no-boundaries', action='store_true',
333                            help="""Index everything except administrative boundaries.""")
334         group.add_argument('--minrank', '-r', type=int, metavar='RANK', default=0,
335                            help='Minimum/starting rank')
336         group.add_argument('--maxrank', '-R', type=int, metavar='RANK', default=30,
337                            help='Maximum/finishing rank')
338
339     @staticmethod
340     def run(args):
341         from .indexer.indexer import Indexer
342
343         indexer = Indexer(args.config.get_libpq_dsn(),
344                           args.threads or _num_system_cpus() or 1)
345
346         if not args.no_boundaries:
347             indexer.index_boundaries(args.minrank, args.maxrank)
348         if not args.boundaries_only:
349             indexer.index_by_rank(args.minrank, args.maxrank)
350
351         if not args.no_boundaries and not args.boundaries_only:
352             indexer.update_status_table()
353
354         return 0
355
356
357 class UpdateRefresh:
358     """\
359     Recompute auxiliary data used by the indexing process.
360
361     These functions must not be run in parallel with other update commands.
362     """
363
364     @staticmethod
365     def add_args(parser):
366         group = parser.add_argument_group('Data arguments')
367         group.add_argument('--postcodes', action='store_true',
368                            help='Update postcode centroid table')
369         group.add_argument('--word-counts', action='store_true',
370                            help='Compute frequency of full-word search terms')
371         group.add_argument('--address-levels', action='store_true',
372                            help='Reimport address level configuration')
373         group.add_argument('--functions', action='store_true',
374                            help='Update the PL/pgSQL functions in the database')
375         group.add_argument('--wiki-data', action='store_true',
376                            help='Update Wikipedia/data importance numbers.')
377         group.add_argument('--importance', action='store_true',
378                            help='Recompute place importances (expensive!)')
379         group.add_argument('--website', action='store_true',
380                            help='Refresh the directory that serves the scripts for the web API')
381         group = parser.add_argument_group('Arguments for function refresh')
382         group.add_argument('--no-diff-updates', action='store_false', dest='diffs',
383                            help='Do not enable code for propagating updates')
384         group.add_argument('--enable-debug-statements', action='store_true',
385                            help='Enable debug warning statements in functions')
386
387     @staticmethod
388     def run(args):
389         from .tools import refresh
390
391         conn = connect(args.config.get_libpq_dsn())
392
393         if args.postcodes:
394             LOG.warning("Update postcodes centroid")
395             refresh.update_postcodes(conn, args.data_dir)
396
397         if args.word_counts:
398             LOG.warning('Recompute frequency of full-word search terms')
399             refresh.recompute_word_counts(conn, args.data_dir)
400
401         if args.address_levels:
402             cfg = Path(args.config.ADDRESS_LEVEL_CONFIG)
403             LOG.warning('Updating address levels from %s', cfg)
404             refresh.load_address_levels_from_file(conn, cfg)
405
406         if args.functions:
407             LOG.warning('Create functions')
408             refresh.create_functions(conn, args.config, args.data_dir,
409                                      args.diffs, args.enable_debug_statements)
410
411         if args.wiki_data:
412             run_legacy_script('setup.php', '--import-wikipedia-articles',
413                               nominatim_env=args, throw_on_fail=True)
414         # Attention: importance MUST come after wiki data import.
415         if args.importance:
416             run_legacy_script('update.php', '--recompute-importance',
417                               nominatim_env=args, throw_on_fail=True)
418         if args.website:
419             run_legacy_script('setup.php', '--setup-website',
420                               nominatim_env=args, throw_on_fail=True)
421
422         conn.close()
423
424         return 0
425
426
427 class AdminCheckDatabase:
428     """\
429     Check that the database is complete and operational.
430     """
431
432     @staticmethod
433     def add_args(parser):
434         pass # No options
435
436     @staticmethod
437     def run(args):
438         return run_legacy_script('check_import_finished.php', nominatim_env=args)
439
440
441 class AdminWarm:
442     """\
443     Warm database caches for search and reverse queries.
444     """
445
446     @staticmethod
447     def add_args(parser):
448         group = parser.add_argument_group('Target arguments')
449         group.add_argument('--search-only', action='store_const', dest='target',
450                            const='search',
451                            help="Only pre-warm tables for search queries")
452         group.add_argument('--reverse-only', action='store_const', dest='target',
453                            const='reverse',
454                            help="Only pre-warm tables for reverse queries")
455
456     @staticmethod
457     def run(args):
458         params = ['warm.php']
459         if args.target == 'reverse':
460             params.append('--reverse-only')
461         if args.target == 'search':
462             params.append('--search-only')
463         return run_legacy_script(*params, nominatim_env=args)
464
465
466 class QueryExport:
467     """\
468     Export addresses as CSV file from the database.
469     """
470
471     @staticmethod
472     def add_args(parser):
473         group = parser.add_argument_group('Output arguments')
474         group.add_argument('--output-type', default='street',
475                            choices=('continent', 'country', 'state', 'county',
476                                     'city', 'suburb', 'street', 'path'),
477                            help='Type of places to output (default: street)')
478         group.add_argument('--output-format',
479                            default='street;suburb;city;county;state;country',
480                            help="""Semicolon-separated list of address types
481                                    (see --output-type). Multiple ranks can be
482                                    merged into one column by simply using a
483                                    comma-separated list.""")
484         group.add_argument('--output-all-postcodes', action='store_true',
485                            help="""List all postcodes for address instead of
486                                    just the most likely one""")
487         group.add_argument('--language',
488                            help="""Preferred language for output
489                                    (use local name, if omitted)""")
490         group = parser.add_argument_group('Filter arguments')
491         group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
492                            help='Export only objects within country')
493         group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
494                            help='Export only children of this OSM node')
495         group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
496                            help='Export only children of this OSM way')
497         group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
498                            help='Export only children of this OSM relation')
499
500
501     @staticmethod
502     def run(args):
503         params = ['export.php',
504                   '--output-type', args.output_type,
505                   '--output-format', args.output_format]
506         if args.output_all_postcodes:
507             params.append('--output-all-postcodes')
508         if args.language:
509             params.extend(('--language', args.language))
510         if args.restrict_to_country:
511             params.extend(('--restrict-to-country', args.restrict_to_country))
512         if args.restrict_to_osm_node:
513             params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
514         if args.restrict_to_osm_way:
515             params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
516         if args.restrict_to_osm_relation:
517             params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
518
519         return run_legacy_script(*params, nominatim_env=args)
520
521 STRUCTURED_QUERY = (
522     ('street', 'housenumber and street'),
523     ('city', 'city, town or village'),
524     ('county', 'county'),
525     ('state', 'state'),
526     ('country', 'country'),
527     ('postalcode', 'postcode')
528 )
529
530 EXTRADATA_PARAMS = (
531     ('addressdetails', 'Include a breakdown of the address into elements.'),
532     ('extratags', """Include additional information if available
533                      (e.g. wikipedia link, opening hours)."""),
534     ('namedetails', 'Include a list of alternative names.')
535 )
536
537 DETAILS_SWITCHES = (
538     ('addressdetails', 'Include a breakdown of the address into elements.'),
539     ('keywords', 'Include a list of name keywords and address keywords.'),
540     ('linkedplaces', 'Include a details of places that are linked with this one.'),
541     ('hierarchy', 'Include details of places lower in the address hierarchy.'),
542     ('group_hierarchy', 'Group the places by type.'),
543     ('polygon_geojson', 'Include geometry of result.')
544 )
545
546 def _add_api_output_arguments(parser):
547     group = parser.add_argument_group('Output arguments')
548     group.add_argument('--format', default='jsonv2',
549                        choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
550                        help='Format of result')
551     for name, desc in EXTRADATA_PARAMS:
552         group.add_argument('--' + name, action='store_true', help=desc)
553
554     group.add_argument('--lang', '--accept-language', metavar='LANGS',
555                        help='Preferred language order for presenting search results')
556     group.add_argument('--polygon-output',
557                        choices=['geojson', 'kml', 'svg', 'text'],
558                        help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
559     group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
560                        help="""Simplify output geometry.
561                                Parameter is difference tolerance in degrees.""")
562
563
564 class APISearch:
565     """\
566     Execute API search query.
567     """
568
569     @staticmethod
570     def add_args(parser):
571         group = parser.add_argument_group('Query arguments')
572         group.add_argument('--query',
573                            help='Free-form query string')
574         for name, desc in STRUCTURED_QUERY:
575             group.add_argument('--' + name, help='Structured query: ' + desc)
576
577         _add_api_output_arguments(parser)
578
579         group = parser.add_argument_group('Result limitation')
580         group.add_argument('--countrycodes', metavar='CC,..',
581                            help='Limit search results to one or more countries.')
582         group.add_argument('--exclude_place_ids', metavar='ID,..',
583                            help='List of search object to be excluded')
584         group.add_argument('--limit', type=int,
585                            help='Limit the number of returned results')
586         group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
587                            help='Preferred area to find search results')
588         group.add_argument('--bounded', action='store_true',
589                            help='Strictly restrict results to viewbox area')
590
591         group = parser.add_argument_group('Other arguments')
592         group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
593                            help='Do not remove duplicates from the result list')
594
595
596     @staticmethod
597     def run(args):
598         if args.query:
599             params = dict(q=args.query)
600         else:
601             params = {k : getattr(args, k) for k, _ in STRUCTURED_QUERY if getattr(args, k)}
602
603         for param, _ in EXTRADATA_PARAMS:
604             if getattr(args, param):
605                 params[param] = '1'
606         for param in ('format', 'countrycodes', 'exclude_place_ids', 'limit', 'viewbox'):
607             if getattr(args, param):
608                 params[param] = getattr(args, param)
609         if args.lang:
610             params['accept-language'] = args.lang
611         if args.polygon_output:
612             params['polygon_' + args.polygon_output] = '1'
613         if args.polygon_threshold:
614             params['polygon_threshold'] = args.polygon_threshold
615         if args.bounded:
616             params['bounded'] = '1'
617         if not args.dedupe:
618             params['dedupe'] = '0'
619
620         return run_api_script('search', args.project_dir,
621                               phpcgi_bin=args.phpcgi_path, params=params)
622
623 class APIReverse:
624     """\
625     Execute API reverse query.
626     """
627
628     @staticmethod
629     def add_args(parser):
630         group = parser.add_argument_group('Query arguments')
631         group.add_argument('--lat', type=float, required=True,
632                            help='Latitude of coordinate to look up (in WGS84)')
633         group.add_argument('--lon', type=float, required=True,
634                            help='Longitude of coordinate to look up (in WGS84)')
635         group.add_argument('--zoom', type=int,
636                            help='Level of detail required for the address')
637
638         _add_api_output_arguments(parser)
639
640
641     @staticmethod
642     def run(args):
643         params = dict(lat=args.lat, lon=args.lon)
644         if args.zoom is not None:
645             params['zoom'] = args.zoom
646
647         for param, _ in EXTRADATA_PARAMS:
648             if getattr(args, param):
649                 params[param] = '1'
650         if args.format:
651             params['format'] = args.format
652         if args.lang:
653             params['accept-language'] = args.lang
654         if args.polygon_output:
655             params['polygon_' + args.polygon_output] = '1'
656         if args.polygon_threshold:
657             params['polygon_threshold'] = args.polygon_threshold
658
659         return run_api_script('reverse', args.project_dir,
660                               phpcgi_bin=args.phpcgi_path, params=params)
661
662
663 class APILookup:
664     """\
665     Execute API reverse query.
666     """
667
668     @staticmethod
669     def add_args(parser):
670         group = parser.add_argument_group('Query arguments')
671         group.add_argument('--id', metavar='OSMID',
672                            action='append', required=True, dest='ids',
673                            help='OSM id to lookup in format <NRW><id> (may be repeated)')
674
675         _add_api_output_arguments(parser)
676
677
678     @staticmethod
679     def run(args):
680         params = dict(osm_ids=','.join(args.ids))
681
682         for param, _ in EXTRADATA_PARAMS:
683             if getattr(args, param):
684                 params[param] = '1'
685         if args.format:
686             params['format'] = args.format
687         if args.lang:
688             params['accept-language'] = args.lang
689         if args.polygon_output:
690             params['polygon_' + args.polygon_output] = '1'
691         if args.polygon_threshold:
692             params['polygon_threshold'] = args.polygon_threshold
693
694         return run_api_script('lookup', args.project_dir,
695                               phpcgi_bin=args.phpcgi_path, params=params)
696
697
698 class APIDetails:
699     """\
700     Execute API lookup query.
701     """
702
703     @staticmethod
704     def add_args(parser):
705         group = parser.add_argument_group('Query arguments')
706         objs = group.add_mutually_exclusive_group(required=True)
707         objs.add_argument('--node', '-n', type=int,
708                           help="Look up the OSM node with the given ID.")
709         objs.add_argument('--way', '-w', type=int,
710                           help="Look up the OSM way with the given ID.")
711         objs.add_argument('--relation', '-r', type=int,
712                           help="Look up the OSM relation with the given ID.")
713         objs.add_argument('--place_id', '-p', type=int,
714                           help='Database internal identifier of the OSM object to look up.')
715         group.add_argument('--class', dest='object_class',
716                            help="""Class type to disambiguated multiple entries
717                                    of the same object.""")
718
719         group = parser.add_argument_group('Output arguments')
720         for name, desc in DETAILS_SWITCHES:
721             group.add_argument('--' + name, action='store_true', help=desc)
722         group.add_argument('--lang', '--accept-language', metavar='LANGS',
723                            help='Preferred language order for presenting search results')
724
725     @staticmethod
726     def run(args):
727         if args.node:
728             params = dict(osmtype='N', osmid=args.node)
729         elif args.way:
730             params = dict(osmtype='W', osmid=args.node)
731         elif args.relation:
732             params = dict(osmtype='R', osmid=args.node)
733         else:
734             params = dict(place_id=args.place_id)
735         if args.object_class:
736             params['class'] = args.object_class
737         for name, _ in DETAILS_SWITCHES:
738             params[name] = '1' if getattr(args, name) else '0'
739
740         return run_api_script('details', args.project_dir,
741                               phpcgi_bin=args.phpcgi_path, params=params)
742
743
744 class APIStatus:
745     """\
746     Execute API status query.
747     """
748
749     @staticmethod
750     def add_args(parser):
751         group = parser.add_argument_group('API parameters')
752         group.add_argument('--format', default='text', choices=['text', 'json'],
753                            help='Format of result')
754
755     @staticmethod
756     def run(args):
757         return run_api_script('status', args.project_dir,
758                               phpcgi_bin=args.phpcgi_path,
759                               params=dict(format=args.format))
760
761
762 def nominatim(**kwargs):
763     """\
764     Command-line tools for importing, updating, administrating and
765     querying the Nominatim database.
766     """
767     parser = CommandlineParser('nominatim', nominatim.__doc__)
768
769     parser.add_subcommand('import', SetupAll)
770     parser.add_subcommand('freeze', SetupFreeze)
771     parser.add_subcommand('replication', UpdateReplication)
772
773     parser.add_subcommand('check-database', AdminCheckDatabase)
774     parser.add_subcommand('warm', AdminWarm)
775
776     parser.add_subcommand('special-phrases', SetupSpecialPhrases)
777
778     parser.add_subcommand('add-data', UpdateAddData)
779     parser.add_subcommand('index', UpdateIndex)
780     parser.add_subcommand('refresh', UpdateRefresh)
781
782     parser.add_subcommand('export', QueryExport)
783
784     if kwargs.get('phpcgi_path'):
785         parser.add_subcommand('search', APISearch)
786         parser.add_subcommand('reverse', APIReverse)
787         parser.add_subcommand('lookup', APILookup)
788         parser.add_subcommand('details', APIDetails)
789         parser.add_subcommand('status', APIStatus)
790     else:
791         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
792
793     return parser.run(**kwargs)