]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/cli.py
optionally hand in command line arguments to CLI functions
[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
14 from .indexer.indexer import Indexer
15
16 def _num_system_cpus():
17     try:
18         cpus = len(os.sched_getaffinity(0))
19     except NotImplementedError:
20         cpus = None
21
22     return cpus or os.cpu_count()
23
24
25 class CommandlineParser:
26     """ Wraps some of the common functions for parsing the command line
27         and setting up subcommands.
28     """
29     def __init__(self, prog, description):
30         self.parser = argparse.ArgumentParser(
31             prog=prog,
32             description=description,
33             formatter_class=argparse.RawDescriptionHelpFormatter)
34
35         self.subs = self.parser.add_subparsers(title='available commands',
36                                                dest='subcommand')
37
38         # Arguments added to every sub-command
39         self.default_args = argparse.ArgumentParser(add_help=False)
40         group = self.default_args.add_argument_group('Default arguments')
41         group.add_argument('-h', '--help', action='help',
42                            help='Show this help message and exit')
43         group.add_argument('-q', '--quiet', action='store_const', const=0,
44                            dest='verbose', default=1,
45                            help='Print only error messages')
46         group.add_argument('-v', '--verbose', action='count', default=1,
47                            help='Increase verboseness of output')
48         group.add_argument('--project-dir', metavar='DIR', default='.',
49                            help='Base directory of the Nominatim installation (default:.)')
50         group.add_argument('-j', '--threads', metavar='NUM', type=int,
51                            help='Number of parallel threads to use')
52
53
54     def add_subcommand(self, name, cmd):
55         """ Add a subcommand to the parser. The subcommand must be a class
56             with a function add_args() that adds the parameters for the
57             subcommand and a run() function that executes the command.
58         """
59         parser = self.subs.add_parser(name, parents=[self.default_args],
60                                       help=cmd.__doc__.split('\n', 1)[0],
61                                       description=cmd.__doc__,
62                                       formatter_class=argparse.RawDescriptionHelpFormatter,
63                                       add_help=False)
64         parser.set_defaults(command=cmd)
65         cmd.add_args(parser)
66
67     def run(self, **kwargs):
68         """ Parse the command line arguments of the program and execute the
69             appropriate subcommand.
70         """
71         args = self.parser.parse_args(args=kwargs.get('cli_args'))
72
73         if args.subcommand is None:
74             self.parser.print_help()
75             return 1
76
77         for arg in ('module_dir', 'osm2pgsql_path', 'phplib_dir', 'data_dir', 'phpcgi_path'):
78             setattr(args, arg, Path(kwargs[arg]))
79         args.project_dir = Path(args.project_dir)
80
81         logging.basicConfig(stream=sys.stderr,
82                             format='%(asctime)s: %(message)s',
83                             datefmt='%Y-%m-%d %H:%M:%S',
84                             level=max(4 - args.verbose, 1) * 10)
85
86         args.config = Configuration(args.project_dir, args.data_dir / 'settings')
87
88         return args.command.run(args)
89
90 ##### Subcommand classes
91 #
92 # Each class needs to implement two functions: add_args() adds the CLI parameters
93 # for the subfunction, run() executes the subcommand.
94 #
95 # The class documentation doubles as the help text for the command. The
96 # first line is also used in the summary when calling the program without
97 # a subcommand.
98 #
99 # No need to document the functions each time.
100 # pylint: disable=C0111
101
102
103 class SetupAll:
104     """\
105     Create a new Nominatim database from an OSM file.
106     """
107
108     @staticmethod
109     def add_args(parser):
110         group_name = parser.add_argument_group('Required arguments')
111         group = group_name.add_mutually_exclusive_group(required=True)
112         group.add_argument('--osm-file',
113                            help='OSM file to be imported.')
114         group.add_argument('--continue', dest='continue_at',
115                            choices=['load-data', 'indexing', 'db-postprocess'],
116                            help='Continue an import that was interrupted')
117         group = parser.add_argument_group('Optional arguments')
118         group.add_argument('--osm2pgsql-cache', metavar='SIZE', type=int,
119                            help='Size of cache to be used by osm2pgsql (in MB)')
120         group.add_argument('--reverse-only', action='store_true',
121                            help='Do not create tables and indexes for searching')
122         group.add_argument('--enable-debug-statements', action='store_true',
123                            help='Include debug warning statements in SQL code')
124         group.add_argument('--no-partitions', action='store_true',
125                            help="""Do not partition search indices
126                                    (speeds up import of single country extracts)""")
127         group.add_argument('--no-updates', action='store_true',
128                            help="""Do not keep tables that are only needed for
129                                    updating the database later""")
130         group = parser.add_argument_group('Expert options')
131         group.add_argument('--ignore-errors', action='store_true',
132                            help='Continue import even when errors in SQL are present')
133         group.add_argument('--index-noanalyse', action='store_true',
134                            help='Do not perform analyse operations during index')
135
136
137     @staticmethod
138     def run(args):
139         params = ['setup.php']
140         if args.osm_file:
141             params.extend(('--all', '--osm-file', args.osm_file))
142         else:
143             if args.continue_at == 'load-data':
144                 params.append('--load-data')
145             if args.continue_at in ('load-data', 'indexing'):
146                 params.append('--index')
147             params.extend(('--create-search-indices', '--create-country-names',
148                            '--setup-website'))
149         if args.osm2pgsql_cache:
150             params.extend(('--osm2pgsql-cache', args.osm2pgsql_cache))
151         if args.reverse_only:
152             params.append('--reverse-only')
153         if args.enable_debug_statements:
154             params.append('--enable-debug-statements')
155         if args.no_partitions:
156             params.append('--no-partitions')
157         if args.no_updates:
158             params.append('--drop')
159         if args.ignore_errors:
160             params.append('--ignore-errors')
161         if args.index_noanalyse:
162             params.append('--index-noanalyse')
163
164         return run_legacy_script(*params, nominatim_env=args)
165
166
167 class SetupFreeze:
168     """\
169     Make database read-only.
170
171     About half of data in the Nominatim database is kept only to be able to
172     keep the data up-to-date with new changes made in OpenStreetMap. This
173     command drops all this data and only keeps the part needed for geocoding
174     itself.
175
176     This command has the same effect as the `--no-updates` option for imports.
177     """
178
179     @staticmethod
180     def add_args(parser):
181         pass # No options
182
183     @staticmethod
184     def run(args):
185         return run_legacy_script('setup.php', '--drop', nominatim_env=args)
186
187
188 class SetupSpecialPhrases:
189     """\
190     Maintain special phrases.
191     """
192
193     @staticmethod
194     def add_args(parser):
195         group = parser.add_argument_group('Input arguments')
196         group.add_argument('--from-wiki', action='store_true',
197                            help='Pull special phrases from the OSM wiki.')
198         group = parser.add_argument_group('Output arguments')
199         group.add_argument('-o', '--output', default='-',
200                            type=argparse.FileType('w', encoding='UTF-8'),
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.name != '<stdout>':
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         indexer = Indexer(args.config.get_libpq_dsn(),
325                           args.threads or _num_system_cpus() or 1)
326
327         if not args.no_boundaries:
328             indexer.index_boundaries(args.minrank, args.maxrank)
329         if not args.boundaries_only:
330             indexer.index_by_rank(args.minrank, args.maxrank)
331
332         if not args.no_boundaries and not args.boundaries_only:
333             indexer.update_status_table()
334
335         return 0
336
337
338 class UpdateRefresh:
339     """\
340     Recompute auxiliary data used by the indexing process.
341
342     These functions must not be run in parallel with other update commands.
343     """
344
345     @staticmethod
346     def add_args(parser):
347         group = parser.add_argument_group('Data arguments')
348         group.add_argument('--postcodes', action='store_true',
349                            help='Update postcode centroid table')
350         group.add_argument('--word-counts', action='store_true',
351                            help='Compute frequency of full-word search terms')
352         group.add_argument('--address-levels', action='store_true',
353                            help='Reimport address level configuration')
354         group.add_argument('--functions', action='store_true',
355                            help='Update the PL/pgSQL functions in the database')
356         group.add_argument('--wiki-data', action='store_true',
357                            help='Update Wikipedia/data importance numbers.')
358         group.add_argument('--importance', action='store_true',
359                            help='Recompute place importances (expensive!)')
360         group.add_argument('--website', action='store_true',
361                            help='Refresh the directory that serves the scripts for the web API')
362         group = parser.add_argument_group('Arguments for function refresh')
363         group.add_argument('--no-diff-updates', action='store_false', dest='diffs',
364                            help='Do not enable code for propagating updates')
365         group.add_argument('--enable-debug-statements', action='store_true',
366                            help='Enable debug warning statements in functions')
367
368     @staticmethod
369     def run(args):
370         if args.postcodes:
371             run_legacy_script('update.php', '--calculate-postcodes',
372                               nominatim_env=args, throw_on_fail=True)
373         if args.word_counts:
374             run_legacy_script('update.php', '--recompute-word-counts',
375                               nominatim_env=args, throw_on_fail=True)
376         if args.address_levels:
377             run_legacy_script('update.php', '--update-address-levels',
378                               nominatim_env=args, throw_on_fail=True)
379         if args.functions:
380             params = ['setup.php', '--create-functions', '--create-partition-functions']
381             if args.diffs:
382                 params.append('--enable-diff-updates')
383             if args.enable_debug_statements:
384                 params.append('--enable-debug-statements')
385             run_legacy_script(*params, nominatim_env=args, throw_on_fail=True)
386         if args.wiki_data:
387             run_legacy_script('setup.php', '--import-wikipedia-articles',
388                               nominatim_env=args, throw_on_fail=True)
389         # Attention: importance MUST come after wiki data import.
390         if args.importance:
391             run_legacy_script('update.php', '--recompute-importance',
392                               nominatim_env=args, throw_on_fail=True)
393         if args.website:
394             run_legacy_script('setup.php', '--setup-website',
395                               nominatim_env=args, throw_on_fail=True)
396
397
398 class AdminCheckDatabase:
399     """\
400     Check that the database is complete and operational.
401     """
402
403     @staticmethod
404     def add_args(parser):
405         pass # No options
406
407     @staticmethod
408     def run(args):
409         return run_legacy_script('check_import_finished.php', nominatim_env=args)
410
411
412 class AdminWarm:
413     """\
414     Warm database caches for search and reverse queries.
415     """
416
417     @staticmethod
418     def add_args(parser):
419         group = parser.add_argument_group('Target arguments')
420         group.add_argument('--search-only', action='store_const', dest='target',
421                            const='search',
422                            help="Only pre-warm tables for search queries")
423         group.add_argument('--reverse-only', action='store_const', dest='target',
424                            const='reverse',
425                            help="Only pre-warm tables for reverse queries")
426
427     @staticmethod
428     def run(args):
429         params = ['warm.php']
430         if args.target == 'reverse':
431             params.append('--reverse-only')
432         if args.target == 'search':
433             params.append('--search-only')
434         return run_legacy_script(*params, nominatim_env=args)
435
436
437 class QueryExport:
438     """\
439     Export addresses as CSV file from the database.
440     """
441
442     @staticmethod
443     def add_args(parser):
444         group = parser.add_argument_group('Output arguments')
445         group.add_argument('--output-type', default='street',
446                            choices=('continent', 'country', 'state', 'county',
447                                     'city', 'suburb', 'street', 'path'),
448                            help='Type of places to output (default: street)')
449         group.add_argument('--output-format',
450                            default='street;suburb;city;county;state;country',
451                            help="""Semicolon-separated list of address types
452                                    (see --output-type). Multiple ranks can be
453                                    merged into one column by simply using a
454                                    comma-separated list.""")
455         group.add_argument('--output-all-postcodes', action='store_true',
456                            help="""List all postcodes for address instead of
457                                    just the most likely one""")
458         group.add_argument('--language',
459                            help="""Preferred language for output
460                                    (use local name, if omitted)""")
461         group = parser.add_argument_group('Filter arguments')
462         group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
463                            help='Export only objects within country')
464         group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
465                            help='Export only children of this OSM node')
466         group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
467                            help='Export only children of this OSM way')
468         group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
469                            help='Export only children of this OSM relation')
470
471
472     @staticmethod
473     def run(args):
474         params = ['export.php',
475                   '--output-type', args.output_type,
476                   '--output-format', args.output_format]
477         if args.output_all_postcodes:
478             params.append('--output-all-postcodes')
479         if args.language:
480             params.extend(('--language', args.language))
481         if args.restrict_to_country:
482             params.extend(('--restrict-to-country', args.restrict_to_country))
483         if args.restrict_to_osm_node:
484             params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
485         if args.restrict_to_osm_way:
486             params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
487         if args.restrict_to_osm_relation:
488             params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
489
490         return run_legacy_script(*params, nominatim_env=args)
491
492 STRUCTURED_QUERY = (
493     ('street', 'housenumber and street'),
494     ('city', 'city, town or village'),
495     ('county', 'county'),
496     ('state', 'state'),
497     ('country', 'country'),
498     ('postalcode', 'postcode')
499 )
500
501 EXTRADATA_PARAMS = (
502     ('addressdetails', 'Include a breakdown of the address into elements.'),
503     ('extratags', """Include additional information if available
504                      (e.g. wikipedia link, opening hours)."""),
505     ('namedetails', 'Include a list of alternative names.')
506 )
507
508 DETAILS_SWITCHES = (
509     ('addressdetails', 'Include a breakdown of the address into elements.'),
510     ('keywords', 'Include a list of name keywords and address keywords.'),
511     ('linkedplaces', 'Include a details of places that are linked with this one.'),
512     ('hierarchy', 'Include details of places lower in the address hierarchy.'),
513     ('group_hierarchy', 'Group the places by type.'),
514     ('polygon_geojson', 'Include geometry of result.')
515 )
516
517 class APISearch:
518     """\
519     Execute API search query.
520     """
521
522     @staticmethod
523     def add_args(parser):
524         group = parser.add_argument_group('Query arguments')
525         group.add_argument('--query',
526                            help='Free-form query string')
527         for name, desc in STRUCTURED_QUERY:
528             group.add_argument('--' + name, help='Structured query: ' + desc)
529
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         group = parser.add_argument_group('Result limitation')
547         group.add_argument('--countrycodes', metavar='CC,..',
548                            help='Limit search results to one or more countries.')
549         group.add_argument('--exclude_place_ids', metavar='ID,..',
550                            help='List of search object to be excluded')
551         group.add_argument('--limit', type=int,
552                            help='Limit the number of returned results')
553         group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
554                            help='Preferred area to find search results')
555         group.add_argument('--bounded', action='store_true',
556                            help='Strictly restrict results to viewbox area')
557
558         group = parser.add_argument_group('Other arguments')
559         group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
560                            help='Do not remove duplicates from the result list')
561
562
563     @staticmethod
564     def run(args):
565         if args.query:
566             params = dict(q=args.query)
567         else:
568             params = {k : getattr(args, k) for k, _ in STRUCTURED_QUERY if getattr(args, k)}
569
570         for param, _ in EXTRADATA_PARAMS:
571             if getattr(args, param):
572                 params[param] = '1'
573         for param in ('format', 'countrycodes', 'exclude_place_ids', 'limit', 'viewbox'):
574             if getattr(args, param):
575                 params[param] = getattr(args, param)
576         if args.lang:
577             params['accept-language'] = args.lang
578         if args.polygon_output:
579             params['polygon_' + args.polygon_output] = '1'
580         if args.polygon_threshold:
581             params['polygon_threshold'] = args.polygon_threshold
582         if args.bounded:
583             params['bounded'] = '1'
584         if not args.dedupe:
585             params['dedupe'] = '0'
586
587         return run_api_script('search', args.project_dir,
588                               phpcgi_bin=args.phpcgi_path, params=params)
589
590 class APIReverse:
591     """\
592     Execute API reverse query.
593     """
594
595     @staticmethod
596     def add_args(parser):
597         group = parser.add_argument_group('Query arguments')
598         group.add_argument('--lat', type=float, required=True,
599                            help='Latitude of coordinate to look up (in WGS84)')
600         group.add_argument('--lon', type=float, required=True,
601                            help='Longitude of coordinate to look up (in WGS84)')
602         group.add_argument('--zoom', type=int,
603                            help='Level of detail required for the address')
604
605         group = parser.add_argument_group('Output arguments')
606         group.add_argument('--format', default='jsonv2',
607                            choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
608                            help='Format of result')
609         for name, desc in EXTRADATA_PARAMS:
610             group.add_argument('--' + name, action='store_true', help=desc)
611
612         group.add_argument('--lang', '--accept-language', metavar='LANGS',
613                            help='Preferred language order for presenting search results')
614         group.add_argument('--polygon-output',
615                            choices=['geojson', 'kml', 'svg', 'text'],
616                            help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
617         group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
618                            help="""Simplify output geometry.
619                                    Parameter is difference tolerance in degrees.""")
620
621
622     @staticmethod
623     def run(args):
624         params = dict(lat=args.lat, lon=args.lon)
625         if args.zoom is not None:
626             params['zoom'] = args.zoom
627
628         for param, _ in EXTRADATA_PARAMS:
629             if getattr(args, param):
630                 params[param] = '1'
631         if args.format:
632             params['format'] = args.format
633         if args.lang:
634             params['accept-language'] = args.lang
635         if args.polygon_output:
636             params['polygon_' + args.polygon_output] = '1'
637         if args.polygon_threshold:
638             params['polygon_threshold'] = args.polygon_threshold
639
640         return run_api_script('reverse', args.project_dir,
641                               phpcgi_bin=args.phpcgi_path, params=params)
642
643
644 class APILookup:
645     """\
646     Execute API reverse query.
647     """
648
649     @staticmethod
650     def add_args(parser):
651         group = parser.add_argument_group('Query arguments')
652         group.add_argument('--id', metavar='OSMID',
653                            action='append', required=True, dest='ids',
654                            help='OSM id to lookup in format <NRW><id> (may be repeated)')
655
656         group = parser.add_argument_group('Output arguments')
657         group.add_argument('--format', default='jsonv2',
658                            choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
659                            help='Format of result')
660         for name, desc in EXTRADATA_PARAMS:
661             group.add_argument('--' + name, action='store_true', help=desc)
662
663         group.add_argument('--lang', '--accept-language', metavar='LANGS',
664                            help='Preferred language order for presenting search results')
665         group.add_argument('--polygon-output',
666                            choices=['geojson', 'kml', 'svg', 'text'],
667                            help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
668         group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
669                            help="""Simplify output geometry.
670                                    Parameter is difference tolerance in degrees.""")
671
672
673     @staticmethod
674     def run(args):
675         params = dict(osm_ids=','.join(args.ids))
676
677         for param, _ in EXTRADATA_PARAMS:
678             if getattr(args, param):
679                 params[param] = '1'
680         if args.format:
681             params['format'] = args.format
682         if args.lang:
683             params['accept-language'] = args.lang
684         if args.polygon_output:
685             params['polygon_' + args.polygon_output] = '1'
686         if args.polygon_threshold:
687             params['polygon_threshold'] = args.polygon_threshold
688
689         return run_api_script('lookup', args.project_dir,
690                               phpcgi_bin=args.phpcgi_path, params=params)
691
692
693 class APIDetails:
694     """\
695     Execute API lookup query.
696     """
697
698     @staticmethod
699     def add_args(parser):
700         group = parser.add_argument_group('Query arguments')
701         objs = group.add_mutually_exclusive_group(required=True)
702         objs.add_argument('--node', '-n', type=int,
703                           help="Look up the OSM node with the given ID.")
704         objs.add_argument('--way', '-w', type=int,
705                           help="Look up the OSM way with the given ID.")
706         objs.add_argument('--relation', '-r', type=int,
707                           help="Look up the OSM relation with the given ID.")
708         objs.add_argument('--place_id', '-p', type=int,
709                           help='Database internal identifier of the OSM object to look up.')
710         group.add_argument('--class', dest='object_class',
711                            help="""Class type to disambiguated multiple entries
712                                    of the same object.""")
713
714         group = parser.add_argument_group('Output arguments')
715         for name, desc in DETAILS_SWITCHES:
716             group.add_argument('--' + name, action='store_true', help=desc)
717         group.add_argument('--lang', '--accept-language', metavar='LANGS',
718                            help='Preferred language order for presenting search results')
719
720     @staticmethod
721     def run(args):
722         if args.node:
723             params = dict(osmtype='N', osmid=args.node)
724         elif args.way:
725             params = dict(osmtype='W', osmid=args.node)
726         elif args.relation:
727             params = dict(osmtype='R', osmid=args.node)
728         else:
729             params = dict(place_id=args.place_id)
730         if args.object_class:
731             params['class'] = args.object_class
732         for name, _ in DETAILS_SWITCHES:
733             params[name] = '1' if getattr(args, name) else '0'
734
735         return run_api_script('details', args.project_dir,
736                               phpcgi_bin=args.phpcgi_path, params=params)
737
738
739 class APIStatus:
740     """\
741     Execute API status query.
742     """
743
744     @staticmethod
745     def add_args(parser):
746         group = parser.add_argument_group('API parameters')
747         group.add_argument('--format', default='text', choices=['text', 'json'],
748                            help='Format of result')
749
750     @staticmethod
751     def run(args):
752         return run_api_script('status', args.project_dir,
753                               phpcgi_bin=args.phpcgi_path,
754                               params=dict(format=args.format))
755
756
757 def nominatim(**kwargs):
758     """\
759     Command-line tools for importing, updating, administrating and
760     querying the Nominatim database.
761     """
762     parser = CommandlineParser('nominatim', nominatim.__doc__)
763
764     parser.add_subcommand('import', SetupAll)
765     parser.add_subcommand('freeze', SetupFreeze)
766     parser.add_subcommand('replication', UpdateReplication)
767
768     parser.add_subcommand('check-database', AdminCheckDatabase)
769     parser.add_subcommand('warm', AdminWarm)
770
771     parser.add_subcommand('special-phrases', SetupSpecialPhrases)
772
773     parser.add_subcommand('add-data', UpdateAddData)
774     parser.add_subcommand('index', UpdateIndex)
775     parser.add_subcommand('refresh', UpdateRefresh)
776
777     parser.add_subcommand('export', QueryExport)
778
779     if kwargs.get('phpcgi_path'):
780         parser.add_subcommand('search', APISearch)
781         parser.add_subcommand('reverse', APIReverse)
782         parser.add_subcommand('lookup', APILookup)
783         parser.add_subcommand('details', APIDetails)
784         parser.add_subcommand('status', APIStatus)
785     else:
786         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
787
788     return parser.run(**kwargs)