]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/cli.py
port address level computation 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 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         conn = psycopg2.connect(args.config.get_libpq_dsn())
376
377         if args.postcodes:
378             LOG.warning("Update postcodes centroid")
379             nominatim.tools.refresh.update_postcodes(conn, args.data_dir)
380         if args.word_counts:
381             LOG.warning('Recompute frequency of full-word search terms')
382             nominatim.tools.refresh.recompute_word_counts(conn, args.data_dir)
383         if args.address_levels:
384             cfg = Path(args.config.ADDRESS_LEVEL_CONFIG)
385             LOG.warning('Updating address levels from %s', cfg)
386             nominatim.tools.refresh.load_address_levels_from_file(conn, cfg)
387         if args.functions:
388             params = ['setup.php', '--create-functions', '--create-partition-functions']
389             if args.diffs:
390                 params.append('--enable-diff-updates')
391             if args.enable_debug_statements:
392                 params.append('--enable-debug-statements')
393             run_legacy_script(*params, nominatim_env=args, throw_on_fail=True)
394         if args.wiki_data:
395             run_legacy_script('setup.php', '--import-wikipedia-articles',
396                               nominatim_env=args, throw_on_fail=True)
397         # Attention: importance MUST come after wiki data import.
398         if args.importance:
399             run_legacy_script('update.php', '--recompute-importance',
400                               nominatim_env=args, throw_on_fail=True)
401         if args.website:
402             run_legacy_script('setup.php', '--setup-website',
403                               nominatim_env=args, throw_on_fail=True)
404
405         conn.close()
406
407         return 0
408
409
410 class AdminCheckDatabase:
411     """\
412     Check that the database is complete and operational.
413     """
414
415     @staticmethod
416     def add_args(parser):
417         pass # No options
418
419     @staticmethod
420     def run(args):
421         return run_legacy_script('check_import_finished.php', nominatim_env=args)
422
423
424 class AdminWarm:
425     """\
426     Warm database caches for search and reverse queries.
427     """
428
429     @staticmethod
430     def add_args(parser):
431         group = parser.add_argument_group('Target arguments')
432         group.add_argument('--search-only', action='store_const', dest='target',
433                            const='search',
434                            help="Only pre-warm tables for search queries")
435         group.add_argument('--reverse-only', action='store_const', dest='target',
436                            const='reverse',
437                            help="Only pre-warm tables for reverse queries")
438
439     @staticmethod
440     def run(args):
441         params = ['warm.php']
442         if args.target == 'reverse':
443             params.append('--reverse-only')
444         if args.target == 'search':
445             params.append('--search-only')
446         return run_legacy_script(*params, nominatim_env=args)
447
448
449 class QueryExport:
450     """\
451     Export addresses as CSV file from the database.
452     """
453
454     @staticmethod
455     def add_args(parser):
456         group = parser.add_argument_group('Output arguments')
457         group.add_argument('--output-type', default='street',
458                            choices=('continent', 'country', 'state', 'county',
459                                     'city', 'suburb', 'street', 'path'),
460                            help='Type of places to output (default: street)')
461         group.add_argument('--output-format',
462                            default='street;suburb;city;county;state;country',
463                            help="""Semicolon-separated list of address types
464                                    (see --output-type). Multiple ranks can be
465                                    merged into one column by simply using a
466                                    comma-separated list.""")
467         group.add_argument('--output-all-postcodes', action='store_true',
468                            help="""List all postcodes for address instead of
469                                    just the most likely one""")
470         group.add_argument('--language',
471                            help="""Preferred language for output
472                                    (use local name, if omitted)""")
473         group = parser.add_argument_group('Filter arguments')
474         group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
475                            help='Export only objects within country')
476         group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
477                            help='Export only children of this OSM node')
478         group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
479                            help='Export only children of this OSM way')
480         group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
481                            help='Export only children of this OSM relation')
482
483
484     @staticmethod
485     def run(args):
486         params = ['export.php',
487                   '--output-type', args.output_type,
488                   '--output-format', args.output_format]
489         if args.output_all_postcodes:
490             params.append('--output-all-postcodes')
491         if args.language:
492             params.extend(('--language', args.language))
493         if args.restrict_to_country:
494             params.extend(('--restrict-to-country', args.restrict_to_country))
495         if args.restrict_to_osm_node:
496             params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
497         if args.restrict_to_osm_way:
498             params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
499         if args.restrict_to_osm_relation:
500             params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
501
502         return run_legacy_script(*params, nominatim_env=args)
503
504 STRUCTURED_QUERY = (
505     ('street', 'housenumber and street'),
506     ('city', 'city, town or village'),
507     ('county', 'county'),
508     ('state', 'state'),
509     ('country', 'country'),
510     ('postalcode', 'postcode')
511 )
512
513 EXTRADATA_PARAMS = (
514     ('addressdetails', 'Include a breakdown of the address into elements.'),
515     ('extratags', """Include additional information if available
516                      (e.g. wikipedia link, opening hours)."""),
517     ('namedetails', 'Include a list of alternative names.')
518 )
519
520 DETAILS_SWITCHES = (
521     ('addressdetails', 'Include a breakdown of the address into elements.'),
522     ('keywords', 'Include a list of name keywords and address keywords.'),
523     ('linkedplaces', 'Include a details of places that are linked with this one.'),
524     ('hierarchy', 'Include details of places lower in the address hierarchy.'),
525     ('group_hierarchy', 'Group the places by type.'),
526     ('polygon_geojson', 'Include geometry of result.')
527 )
528
529 def _add_api_output_arguments(parser):
530     group = parser.add_argument_group('Output arguments')
531     group.add_argument('--format', default='jsonv2',
532                        choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
533                        help='Format of result')
534     for name, desc in EXTRADATA_PARAMS:
535         group.add_argument('--' + name, action='store_true', help=desc)
536
537     group.add_argument('--lang', '--accept-language', metavar='LANGS',
538                        help='Preferred language order for presenting search results')
539     group.add_argument('--polygon-output',
540                        choices=['geojson', 'kml', 'svg', 'text'],
541                        help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
542     group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
543                        help="""Simplify output geometry.
544                                Parameter is difference tolerance in degrees.""")
545
546
547 class APISearch:
548     """\
549     Execute API search query.
550     """
551
552     @staticmethod
553     def add_args(parser):
554         group = parser.add_argument_group('Query arguments')
555         group.add_argument('--query',
556                            help='Free-form query string')
557         for name, desc in STRUCTURED_QUERY:
558             group.add_argument('--' + name, help='Structured query: ' + desc)
559
560         _add_api_output_arguments(parser)
561
562         group = parser.add_argument_group('Result limitation')
563         group.add_argument('--countrycodes', metavar='CC,..',
564                            help='Limit search results to one or more countries.')
565         group.add_argument('--exclude_place_ids', metavar='ID,..',
566                            help='List of search object to be excluded')
567         group.add_argument('--limit', type=int,
568                            help='Limit the number of returned results')
569         group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
570                            help='Preferred area to find search results')
571         group.add_argument('--bounded', action='store_true',
572                            help='Strictly restrict results to viewbox area')
573
574         group = parser.add_argument_group('Other arguments')
575         group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
576                            help='Do not remove duplicates from the result list')
577
578
579     @staticmethod
580     def run(args):
581         if args.query:
582             params = dict(q=args.query)
583         else:
584             params = {k : getattr(args, k) for k, _ in STRUCTURED_QUERY if getattr(args, k)}
585
586         for param, _ in EXTRADATA_PARAMS:
587             if getattr(args, param):
588                 params[param] = '1'
589         for param in ('format', 'countrycodes', 'exclude_place_ids', 'limit', 'viewbox'):
590             if getattr(args, param):
591                 params[param] = getattr(args, param)
592         if args.lang:
593             params['accept-language'] = args.lang
594         if args.polygon_output:
595             params['polygon_' + args.polygon_output] = '1'
596         if args.polygon_threshold:
597             params['polygon_threshold'] = args.polygon_threshold
598         if args.bounded:
599             params['bounded'] = '1'
600         if not args.dedupe:
601             params['dedupe'] = '0'
602
603         return run_api_script('search', args.project_dir,
604                               phpcgi_bin=args.phpcgi_path, params=params)
605
606 class APIReverse:
607     """\
608     Execute API reverse query.
609     """
610
611     @staticmethod
612     def add_args(parser):
613         group = parser.add_argument_group('Query arguments')
614         group.add_argument('--lat', type=float, required=True,
615                            help='Latitude of coordinate to look up (in WGS84)')
616         group.add_argument('--lon', type=float, required=True,
617                            help='Longitude of coordinate to look up (in WGS84)')
618         group.add_argument('--zoom', type=int,
619                            help='Level of detail required for the address')
620
621         _add_api_output_arguments(parser)
622
623
624     @staticmethod
625     def run(args):
626         params = dict(lat=args.lat, lon=args.lon)
627         if args.zoom is not None:
628             params['zoom'] = args.zoom
629
630         for param, _ in EXTRADATA_PARAMS:
631             if getattr(args, param):
632                 params[param] = '1'
633         if args.format:
634             params['format'] = args.format
635         if args.lang:
636             params['accept-language'] = args.lang
637         if args.polygon_output:
638             params['polygon_' + args.polygon_output] = '1'
639         if args.polygon_threshold:
640             params['polygon_threshold'] = args.polygon_threshold
641
642         return run_api_script('reverse', args.project_dir,
643                               phpcgi_bin=args.phpcgi_path, params=params)
644
645
646 class APILookup:
647     """\
648     Execute API reverse query.
649     """
650
651     @staticmethod
652     def add_args(parser):
653         group = parser.add_argument_group('Query arguments')
654         group.add_argument('--id', metavar='OSMID',
655                            action='append', required=True, dest='ids',
656                            help='OSM id to lookup in format <NRW><id> (may be repeated)')
657
658         _add_api_output_arguments(parser)
659
660
661     @staticmethod
662     def run(args):
663         params = dict(osm_ids=','.join(args.ids))
664
665         for param, _ in EXTRADATA_PARAMS:
666             if getattr(args, param):
667                 params[param] = '1'
668         if args.format:
669             params['format'] = args.format
670         if args.lang:
671             params['accept-language'] = args.lang
672         if args.polygon_output:
673             params['polygon_' + args.polygon_output] = '1'
674         if args.polygon_threshold:
675             params['polygon_threshold'] = args.polygon_threshold
676
677         return run_api_script('lookup', args.project_dir,
678                               phpcgi_bin=args.phpcgi_path, params=params)
679
680
681 class APIDetails:
682     """\
683     Execute API lookup query.
684     """
685
686     @staticmethod
687     def add_args(parser):
688         group = parser.add_argument_group('Query arguments')
689         objs = group.add_mutually_exclusive_group(required=True)
690         objs.add_argument('--node', '-n', type=int,
691                           help="Look up the OSM node with the given ID.")
692         objs.add_argument('--way', '-w', type=int,
693                           help="Look up the OSM way with the given ID.")
694         objs.add_argument('--relation', '-r', type=int,
695                           help="Look up the OSM relation with the given ID.")
696         objs.add_argument('--place_id', '-p', type=int,
697                           help='Database internal identifier of the OSM object to look up.')
698         group.add_argument('--class', dest='object_class',
699                            help="""Class type to disambiguated multiple entries
700                                    of the same object.""")
701
702         group = parser.add_argument_group('Output arguments')
703         for name, desc in DETAILS_SWITCHES:
704             group.add_argument('--' + name, action='store_true', help=desc)
705         group.add_argument('--lang', '--accept-language', metavar='LANGS',
706                            help='Preferred language order for presenting search results')
707
708     @staticmethod
709     def run(args):
710         if args.node:
711             params = dict(osmtype='N', osmid=args.node)
712         elif args.way:
713             params = dict(osmtype='W', osmid=args.node)
714         elif args.relation:
715             params = dict(osmtype='R', osmid=args.node)
716         else:
717             params = dict(place_id=args.place_id)
718         if args.object_class:
719             params['class'] = args.object_class
720         for name, _ in DETAILS_SWITCHES:
721             params[name] = '1' if getattr(args, name) else '0'
722
723         return run_api_script('details', args.project_dir,
724                               phpcgi_bin=args.phpcgi_path, params=params)
725
726
727 class APIStatus:
728     """\
729     Execute API status query.
730     """
731
732     @staticmethod
733     def add_args(parser):
734         group = parser.add_argument_group('API parameters')
735         group.add_argument('--format', default='text', choices=['text', 'json'],
736                            help='Format of result')
737
738     @staticmethod
739     def run(args):
740         return run_api_script('status', args.project_dir,
741                               phpcgi_bin=args.phpcgi_path,
742                               params=dict(format=args.format))
743
744
745 def nominatim(**kwargs):
746     """\
747     Command-line tools for importing, updating, administrating and
748     querying the Nominatim database.
749     """
750     parser = CommandlineParser('nominatim', nominatim.__doc__)
751
752     parser.add_subcommand('import', SetupAll)
753     parser.add_subcommand('freeze', SetupFreeze)
754     parser.add_subcommand('replication', UpdateReplication)
755
756     parser.add_subcommand('check-database', AdminCheckDatabase)
757     parser.add_subcommand('warm', AdminWarm)
758
759     parser.add_subcommand('special-phrases', SetupSpecialPhrases)
760
761     parser.add_subcommand('add-data', UpdateAddData)
762     parser.add_subcommand('index', UpdateIndex)
763     parser.add_subcommand('refresh', UpdateRefresh)
764
765     parser.add_subcommand('export', QueryExport)
766
767     if kwargs.get('phpcgi_path'):
768         parser.add_subcommand('search', APISearch)
769         parser.add_subcommand('reverse', APIReverse)
770         parser.add_subcommand('lookup', APILookup)
771         parser.add_subcommand('details', APIDetails)
772         parser.add_subcommand('status', APIStatus)
773     else:
774         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
775
776     return parser.run(**kwargs)