]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/cli.py
convert functon creation 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         params = ['update.php']
238         if args.init:
239             params.append('--init-updates')
240             if not args.update_functions:
241                 params.append('--no-update-functions')
242         elif args.check_for_updates:
243             params.append('--check-for-updates')
244         else:
245             if args.once:
246                 params.append('--import-osmosis')
247             else:
248                 params.append('--import-osmosis-all')
249             if not args.do_index:
250                 params.append('--no-index')
251
252         return run_legacy_script(*params, nominatim_env=args)
253
254
255 class UpdateAddData:
256     """\
257     Add additional data from a file or an online source.
258
259     Data is only imported, not indexed. You need to call `nominatim-update index`
260     to complete the process.
261     """
262
263     @staticmethod
264     def add_args(parser):
265         group_name = parser.add_argument_group('Source')
266         group = group_name.add_mutually_exclusive_group(required=True)
267         group.add_argument('--file', metavar='FILE',
268                            help='Import data from an OSM file')
269         group.add_argument('--diff', metavar='FILE',
270                            help='Import data from an OSM diff file')
271         group.add_argument('--node', metavar='ID', type=int,
272                            help='Import a single node from the API')
273         group.add_argument('--way', metavar='ID', type=int,
274                            help='Import a single way from the API')
275         group.add_argument('--relation', metavar='ID', type=int,
276                            help='Import a single relation from the API')
277         group.add_argument('--tiger-data', metavar='DIR',
278                            help='Add housenumbers from the US TIGER census database.')
279         group = parser.add_argument_group('Extra arguments')
280         group.add_argument('--use-main-api', action='store_true',
281                            help='Use OSM API instead of Overpass to download objects')
282
283     @staticmethod
284     def run(args):
285         if args.tiger_data:
286             os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
287             return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
288
289         params = ['update.php']
290         if args.file:
291             params.extend(('--import-file', args.file))
292         elif args.diff:
293             params.extend(('--import-diff', args.diff))
294         elif args.node:
295             params.extend(('--import-node', args.node))
296         elif args.way:
297             params.extend(('--import-way', args.way))
298         elif args.relation:
299             params.extend(('--import-relation', args.relation))
300         if args.use_main_api:
301             params.append('--use-main-api')
302         return run_legacy_script(*params, nominatim_env=args)
303
304
305 class UpdateIndex:
306     """\
307     Reindex all new and modified data.
308     """
309
310     @staticmethod
311     def add_args(parser):
312         group = parser.add_argument_group('Filter arguments')
313         group.add_argument('--boundaries-only', action='store_true',
314                            help="""Index only administrative boundaries.""")
315         group.add_argument('--no-boundaries', action='store_true',
316                            help="""Index everything except administrative boundaries.""")
317         group.add_argument('--minrank', '-r', type=int, metavar='RANK', default=0,
318                            help='Minimum/starting rank')
319         group.add_argument('--maxrank', '-R', type=int, metavar='RANK', default=30,
320                            help='Maximum/finishing rank')
321
322     @staticmethod
323     def run(args):
324         from .indexer.indexer import Indexer
325
326         indexer = Indexer(args.config.get_libpq_dsn(),
327                           args.threads or _num_system_cpus() or 1)
328
329         if not args.no_boundaries:
330             indexer.index_boundaries(args.minrank, args.maxrank)
331         if not args.boundaries_only:
332             indexer.index_by_rank(args.minrank, args.maxrank)
333
334         if not args.no_boundaries and not args.boundaries_only:
335             indexer.update_status_table()
336
337         return 0
338
339
340 class UpdateRefresh:
341     """\
342     Recompute auxiliary data used by the indexing process.
343
344     These functions must not be run in parallel with other update commands.
345     """
346
347     @staticmethod
348     def add_args(parser):
349         group = parser.add_argument_group('Data arguments')
350         group.add_argument('--postcodes', action='store_true',
351                            help='Update postcode centroid table')
352         group.add_argument('--word-counts', action='store_true',
353                            help='Compute frequency of full-word search terms')
354         group.add_argument('--address-levels', action='store_true',
355                            help='Reimport address level configuration')
356         group.add_argument('--functions', action='store_true',
357                            help='Update the PL/pgSQL functions in the database')
358         group.add_argument('--wiki-data', action='store_true',
359                            help='Update Wikipedia/data importance numbers.')
360         group.add_argument('--importance', action='store_true',
361                            help='Recompute place importances (expensive!)')
362         group.add_argument('--website', action='store_true',
363                            help='Refresh the directory that serves the scripts for the web API')
364         group = parser.add_argument_group('Arguments for function refresh')
365         group.add_argument('--no-diff-updates', action='store_false', dest='diffs',
366                            help='Do not enable code for propagating updates')
367         group.add_argument('--enable-debug-statements', action='store_true',
368                            help='Enable debug warning statements in functions')
369
370     @staticmethod
371     def run(args):
372         from .tools import refresh
373
374         conn = connect(args.config.get_libpq_dsn())
375
376         if args.postcodes:
377             LOG.warning("Update postcodes centroid")
378             refresh.update_postcodes(conn, args.data_dir)
379
380         if args.word_counts:
381             LOG.warning('Recompute frequency of full-word search terms')
382             refresh.recompute_word_counts(conn, args.data_dir)
383
384         if args.address_levels:
385             cfg = Path(args.config.ADDRESS_LEVEL_CONFIG)
386             LOG.warning('Updating address levels from %s', cfg)
387             refresh.load_address_levels_from_file(conn, cfg)
388
389         if args.functions:
390             LOG.warning('Create functions')
391             refresh.create_functions(conn, args.config, args.data_dir,
392                                      args.diffs, args.enable_debug_statements)
393
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)