]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/cli.py
4388902dcbbd1ecbe9630154ac4d5d2375f00ec6
[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 import psycopg2
12
13 from .config import Configuration
14 from .tools.exec_utils import run_legacy_script, run_api_script
15
16 LOG = logging.getLogger()
17
18 def _num_system_cpus():
19     try:
20         cpus = len(os.sched_getaffinity(0))
21     except NotImplementedError:
22         cpus = None
23
24     return cpus or os.cpu_count()
25
26
27 class CommandlineParser:
28     """ Wraps some of the common functions for parsing the command line
29         and setting up subcommands.
30     """
31     def __init__(self, prog, description):
32         self.parser = argparse.ArgumentParser(
33             prog=prog,
34             description=description,
35             formatter_class=argparse.RawDescriptionHelpFormatter)
36
37         self.subs = self.parser.add_subparsers(title='available commands',
38                                                dest='subcommand')
39
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')
54
55
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.
60         """
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,
65                                       add_help=False)
66         parser.set_defaults(command=cmd)
67         cmd.add_args(parser)
68
69     def run(self, **kwargs):
70         """ Parse the command line arguments of the program and execute the
71             appropriate subcommand.
72         """
73         args = self.parser.parse_args(args=kwargs.get('cli_args'))
74
75         if args.subcommand is None:
76             self.parser.print_help()
77             return 1
78
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)
82
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)
87
88         args.config = Configuration(args.project_dir, args.data_dir / 'settings')
89
90         return args.command.run(args)
91
92 ##### Subcommand classes
93 #
94 # Each class needs to implement two functions: add_args() adds the CLI parameters
95 # for the subfunction, run() executes the subcommand.
96 #
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
99 # a subcommand.
100 #
101 # No need to document the functions each time.
102 # pylint: disable=C0111
103
104
105 class SetupAll:
106     """\
107     Create a new Nominatim database from an OSM file.
108     """
109
110     @staticmethod
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')
137
138
139     @staticmethod
140     def run(args):
141         params = ['setup.php']
142         if args.osm_file:
143             params.extend(('--all', '--osm-file', args.osm_file))
144         else:
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',
150                            '--setup-website'))
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')
159         if args.no_updates:
160             params.append('--drop')
161         if args.ignore_errors:
162             params.append('--ignore-errors')
163         if args.index_noanalyse:
164             params.append('--index-noanalyse')
165
166         return run_legacy_script(*params, nominatim_env=args)
167
168
169 class SetupFreeze:
170     """\
171     Make database read-only.
172
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
176     itself.
177
178     This command has the same effect as the `--no-updates` option for imports.
179     """
180
181     @staticmethod
182     def add_args(parser):
183         pass # No options
184
185     @staticmethod
186     def run(args):
187         return run_legacy_script('setup.php', '--drop', nominatim_env=args)
188
189
190 class SetupSpecialPhrases:
191     """\
192     Maintain special phrases.
193     """
194
195     @staticmethod
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.""")
204
205     @staticmethod
206     def run(args):
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)
210
211
212 class UpdateReplication:
213     """\
214     Update the database using an online replication service.
215     """
216
217     @staticmethod
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""")
235
236     @staticmethod
237     def run(args):
238         params = ['update.php']
239         if args.init:
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')
245         else:
246             if args.once:
247                 params.append('--import-osmosis')
248             else:
249                 params.append('--import-osmosis-all')
250             if not args.do_index:
251                 params.append('--no-index')
252
253         return run_legacy_script(*params, nominatim_env=args)
254
255
256 class UpdateAddData:
257     """\
258     Add additional data from a file or an online source.
259
260     Data is only imported, not indexed. You need to call `nominatim-update index`
261     to complete the process.
262     """
263
264     @staticmethod
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')
283
284     @staticmethod
285     def run(args):
286         if args.tiger_data:
287             os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
288             return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
289
290         params = ['update.php']
291         if args.file:
292             params.extend(('--import-file', args.file))
293         elif args.diff:
294             params.extend(('--import-diff', args.diff))
295         elif args.node:
296             params.extend(('--import-node', args.node))
297         elif args.way:
298             params.extend(('--import-way', args.way))
299         elif args.relation:
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)
304
305
306 class UpdateIndex:
307     """\
308     Reindex all new and modified data.
309     """
310
311     @staticmethod
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')
322
323     @staticmethod
324     def run(args):
325         from .indexer.indexer import Indexer
326
327         indexer = Indexer(args.config.get_libpq_dsn(),
328                           args.threads or _num_system_cpus() or 1)
329
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)
334
335         if not args.no_boundaries and not args.boundaries_only:
336             indexer.update_status_table()
337
338         return 0
339
340
341 class UpdateRefresh:
342     """\
343     Recompute auxiliary data used by the indexing process.
344
345     These functions must not be run in parallel with other update commands.
346     """
347
348     @staticmethod
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')
370
371     @staticmethod
372     def run(args):
373         import nominatim.tools.refresh
374
375         with psycopg2.connect(args.config.get_libpq_dsn()) as conn:
376             if args.postcodes:
377                 LOG.warning("Update postcodes centroid")
378                 nominatim.tools.refresh.update_postcodes(conn, args.data_dir)
379             if args.word_counts:
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)
385             if args.functions:
386                 params = ['setup.php', '--create-functions', '--create-partition-functions']
387                 if args.diffs:
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)
392             if args.wiki_data:
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.
396             if args.importance:
397                 run_legacy_script('update.php', '--recompute-importance',
398                                   nominatim_env=args, throw_on_fail=True)
399             if args.website:
400                 run_legacy_script('setup.php', '--setup-website',
401                                   nominatim_env=args, throw_on_fail=True)
402         return 0
403
404
405 class AdminCheckDatabase:
406     """\
407     Check that the database is complete and operational.
408     """
409
410     @staticmethod
411     def add_args(parser):
412         pass # No options
413
414     @staticmethod
415     def run(args):
416         return run_legacy_script('check_import_finished.php', nominatim_env=args)
417
418
419 class AdminWarm:
420     """\
421     Warm database caches for search and reverse queries.
422     """
423
424     @staticmethod
425     def add_args(parser):
426         group = parser.add_argument_group('Target arguments')
427         group.add_argument('--search-only', action='store_const', dest='target',
428                            const='search',
429                            help="Only pre-warm tables for search queries")
430         group.add_argument('--reverse-only', action='store_const', dest='target',
431                            const='reverse',
432                            help="Only pre-warm tables for reverse queries")
433
434     @staticmethod
435     def run(args):
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)
442
443
444 class QueryExport:
445     """\
446     Export addresses as CSV file from the database.
447     """
448
449     @staticmethod
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')
477
478
479     @staticmethod
480     def run(args):
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')
486         if args.language:
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))
496
497         return run_legacy_script(*params, nominatim_env=args)
498
499 STRUCTURED_QUERY = (
500     ('street', 'housenumber and street'),
501     ('city', 'city, town or village'),
502     ('county', 'county'),
503     ('state', 'state'),
504     ('country', 'country'),
505     ('postalcode', 'postcode')
506 )
507
508 EXTRADATA_PARAMS = (
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.')
513 )
514
515 DETAILS_SWITCHES = (
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.')
522 )
523
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)
531
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.""")
540
541
542 class APISearch:
543     """\
544     Execute API search query.
545     """
546
547     @staticmethod
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)
554
555         _add_api_output_arguments(parser)
556
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')
568
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')
572
573
574     @staticmethod
575     def run(args):
576         if args.query:
577             params = dict(q=args.query)
578         else:
579             params = {k : getattr(args, k) for k, _ in STRUCTURED_QUERY if getattr(args, k)}
580
581         for param, _ in EXTRADATA_PARAMS:
582             if getattr(args, param):
583                 params[param] = '1'
584         for param in ('format', 'countrycodes', 'exclude_place_ids', 'limit', 'viewbox'):
585             if getattr(args, param):
586                 params[param] = getattr(args, param)
587         if args.lang:
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
593         if args.bounded:
594             params['bounded'] = '1'
595         if not args.dedupe:
596             params['dedupe'] = '0'
597
598         return run_api_script('search', args.project_dir,
599                               phpcgi_bin=args.phpcgi_path, params=params)
600
601 class APIReverse:
602     """\
603     Execute API reverse query.
604     """
605
606     @staticmethod
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')
615
616         _add_api_output_arguments(parser)
617
618
619     @staticmethod
620     def run(args):
621         params = dict(lat=args.lat, lon=args.lon)
622         if args.zoom is not None:
623             params['zoom'] = args.zoom
624
625         for param, _ in EXTRADATA_PARAMS:
626             if getattr(args, param):
627                 params[param] = '1'
628         if args.format:
629             params['format'] = args.format
630         if args.lang:
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
636
637         return run_api_script('reverse', args.project_dir,
638                               phpcgi_bin=args.phpcgi_path, params=params)
639
640
641 class APILookup:
642     """\
643     Execute API reverse query.
644     """
645
646     @staticmethod
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)')
652
653         _add_api_output_arguments(parser)
654
655
656     @staticmethod
657     def run(args):
658         params = dict(osm_ids=','.join(args.ids))
659
660         for param, _ in EXTRADATA_PARAMS:
661             if getattr(args, param):
662                 params[param] = '1'
663         if args.format:
664             params['format'] = args.format
665         if args.lang:
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
671
672         return run_api_script('lookup', args.project_dir,
673                               phpcgi_bin=args.phpcgi_path, params=params)
674
675
676 class APIDetails:
677     """\
678     Execute API lookup query.
679     """
680
681     @staticmethod
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.""")
696
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')
702
703     @staticmethod
704     def run(args):
705         if args.node:
706             params = dict(osmtype='N', osmid=args.node)
707         elif args.way:
708             params = dict(osmtype='W', osmid=args.node)
709         elif args.relation:
710             params = dict(osmtype='R', osmid=args.node)
711         else:
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'
717
718         return run_api_script('details', args.project_dir,
719                               phpcgi_bin=args.phpcgi_path, params=params)
720
721
722 class APIStatus:
723     """\
724     Execute API status query.
725     """
726
727     @staticmethod
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')
732
733     @staticmethod
734     def run(args):
735         return run_api_script('status', args.project_dir,
736                               phpcgi_bin=args.phpcgi_path,
737                               params=dict(format=args.format))
738
739
740 def nominatim(**kwargs):
741     """\
742     Command-line tools for importing, updating, administrating and
743     querying the Nominatim database.
744     """
745     parser = CommandlineParser('nominatim', nominatim.__doc__)
746
747     parser.add_subcommand('import', SetupAll)
748     parser.add_subcommand('freeze', SetupFreeze)
749     parser.add_subcommand('replication', UpdateReplication)
750
751     parser.add_subcommand('check-database', AdminCheckDatabase)
752     parser.add_subcommand('warm', AdminWarm)
753
754     parser.add_subcommand('special-phrases', SetupSpecialPhrases)
755
756     parser.add_subcommand('add-data', UpdateAddData)
757     parser.add_subcommand('index', UpdateIndex)
758     parser.add_subcommand('refresh', UpdateRefresh)
759
760     parser.add_subcommand('export', QueryExport)
761
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)
768     else:
769         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
770
771     return parser.run(**kwargs)