]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/cli.py
port replication update function 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 datetime as dt
6 import os
7 import sys
8 import time
9 import argparse
10 import logging
11 from pathlib import Path
12
13 from .config import Configuration
14 from .tools.exec_utils import run_legacy_script, run_api_script
15 from .db.connection import connect
16 from .db import status
17
18 LOG = logging.getLogger()
19
20 def _num_system_cpus():
21     try:
22         cpus = len(os.sched_getaffinity(0))
23     except NotImplementedError:
24         cpus = None
25
26     return cpus or os.cpu_count()
27
28
29 class CommandlineParser:
30     """ Wraps some of the common functions for parsing the command line
31         and setting up subcommands.
32     """
33     def __init__(self, prog, description):
34         self.parser = argparse.ArgumentParser(
35             prog=prog,
36             description=description,
37             formatter_class=argparse.RawDescriptionHelpFormatter)
38
39         self.subs = self.parser.add_subparsers(title='available commands',
40                                                dest='subcommand')
41
42         # Arguments added to every sub-command
43         self.default_args = argparse.ArgumentParser(add_help=False)
44         group = self.default_args.add_argument_group('Default arguments')
45         group.add_argument('-h', '--help', action='help',
46                            help='Show this help message and exit')
47         group.add_argument('-q', '--quiet', action='store_const', const=0,
48                            dest='verbose', default=1,
49                            help='Print only error messages')
50         group.add_argument('-v', '--verbose', action='count', default=1,
51                            help='Increase verboseness of output')
52         group.add_argument('--project-dir', metavar='DIR', default='.',
53                            help='Base directory of the Nominatim installation (default:.)')
54         group.add_argument('-j', '--threads', metavar='NUM', type=int,
55                            help='Number of parallel threads to use')
56
57
58     def add_subcommand(self, name, cmd):
59         """ Add a subcommand to the parser. The subcommand must be a class
60             with a function add_args() that adds the parameters for the
61             subcommand and a run() function that executes the command.
62         """
63         parser = self.subs.add_parser(name, parents=[self.default_args],
64                                       help=cmd.__doc__.split('\n', 1)[0],
65                                       description=cmd.__doc__,
66                                       formatter_class=argparse.RawDescriptionHelpFormatter,
67                                       add_help=False)
68         parser.set_defaults(command=cmd)
69         cmd.add_args(parser)
70
71     def run(self, **kwargs):
72         """ Parse the command line arguments of the program and execute the
73             appropriate subcommand.
74         """
75         args = self.parser.parse_args(args=kwargs.get('cli_args'))
76
77         if args.subcommand is None:
78             self.parser.print_help()
79             return 1
80
81         for arg in ('module_dir', 'osm2pgsql_path', 'phplib_dir', 'data_dir', 'phpcgi_path'):
82             setattr(args, arg, Path(kwargs[arg]))
83         args.project_dir = Path(args.project_dir)
84
85         logging.basicConfig(stream=sys.stderr,
86                             format='%(asctime)s: %(message)s',
87                             datefmt='%Y-%m-%d %H:%M:%S',
88                             level=max(4 - args.verbose, 1) * 10)
89
90         args.config = Configuration(args.project_dir, args.data_dir / 'settings')
91
92         return args.command.run(args)
93
94
95 def _osm2pgsql_options_from_args(args, default_cache, default_threads):
96     """ Set up the stanadrd osm2pgsql from the command line arguments.
97     """
98     return dict(osm2pgsql=args.osm2pgsql_path,
99                 osm2pgsql_cache=args.osm2pgsql_cache or default_cache,
100                 osm2pgsql_style=args.config.get_import_style_file(),
101                 threads=args.threads or default_threads,
102                 dsn=args.config.get_libpq_dsn(),
103                 flatnode_file=args.config.FLATNODE_FILE)
104
105 ##### Subcommand classes
106 #
107 # Each class needs to implement two functions: add_args() adds the CLI parameters
108 # for the subfunction, run() executes the subcommand.
109 #
110 # The class documentation doubles as the help text for the command. The
111 # first line is also used in the summary when calling the program without
112 # a subcommand.
113 #
114 # No need to document the functions each time.
115 # pylint: disable=C0111
116
117
118 class SetupAll:
119     """\
120     Create a new Nominatim database from an OSM file.
121     """
122
123     @staticmethod
124     def add_args(parser):
125         group_name = parser.add_argument_group('Required arguments')
126         group = group_name.add_mutually_exclusive_group(required=True)
127         group.add_argument('--osm-file',
128                            help='OSM file to be imported.')
129         group.add_argument('--continue', dest='continue_at',
130                            choices=['load-data', 'indexing', 'db-postprocess'],
131                            help='Continue an import that was interrupted')
132         group = parser.add_argument_group('Optional arguments')
133         group.add_argument('--osm2pgsql-cache', metavar='SIZE', type=int,
134                            help='Size of cache to be used by osm2pgsql (in MB)')
135         group.add_argument('--reverse-only', action='store_true',
136                            help='Do not create tables and indexes for searching')
137         group.add_argument('--enable-debug-statements', action='store_true',
138                            help='Include debug warning statements in SQL code')
139         group.add_argument('--no-partitions', action='store_true',
140                            help="""Do not partition search indices
141                                    (speeds up import of single country extracts)""")
142         group.add_argument('--no-updates', action='store_true',
143                            help="""Do not keep tables that are only needed for
144                                    updating the database later""")
145         group = parser.add_argument_group('Expert options')
146         group.add_argument('--ignore-errors', action='store_true',
147                            help='Continue import even when errors in SQL are present')
148         group.add_argument('--index-noanalyse', action='store_true',
149                            help='Do not perform analyse operations during index')
150
151
152     @staticmethod
153     def run(args):
154         params = ['setup.php']
155         if args.osm_file:
156             params.extend(('--all', '--osm-file', args.osm_file))
157         else:
158             if args.continue_at == 'load-data':
159                 params.append('--load-data')
160             if args.continue_at in ('load-data', 'indexing'):
161                 params.append('--index')
162             params.extend(('--create-search-indices', '--create-country-names',
163                            '--setup-website'))
164         if args.osm2pgsql_cache:
165             params.extend(('--osm2pgsql-cache', args.osm2pgsql_cache))
166         if args.reverse_only:
167             params.append('--reverse-only')
168         if args.enable_debug_statements:
169             params.append('--enable-debug-statements')
170         if args.no_partitions:
171             params.append('--no-partitions')
172         if args.no_updates:
173             params.append('--drop')
174         if args.ignore_errors:
175             params.append('--ignore-errors')
176         if args.index_noanalyse:
177             params.append('--index-noanalyse')
178
179         return run_legacy_script(*params, nominatim_env=args)
180
181
182 class SetupFreeze:
183     """\
184     Make database read-only.
185
186     About half of data in the Nominatim database is kept only to be able to
187     keep the data up-to-date with new changes made in OpenStreetMap. This
188     command drops all this data and only keeps the part needed for geocoding
189     itself.
190
191     This command has the same effect as the `--no-updates` option for imports.
192     """
193
194     @staticmethod
195     def add_args(parser):
196         pass # No options
197
198     @staticmethod
199     def run(args):
200         return run_legacy_script('setup.php', '--drop', nominatim_env=args)
201
202
203 class SetupSpecialPhrases:
204     """\
205     Maintain special phrases.
206     """
207
208     @staticmethod
209     def add_args(parser):
210         group = parser.add_argument_group('Input arguments')
211         group.add_argument('--from-wiki', action='store_true',
212                            help='Pull special phrases from the OSM wiki.')
213         group = parser.add_argument_group('Output arguments')
214         group.add_argument('-o', '--output', default='-',
215                            help="""File to write the preprocessed phrases to.
216                                    If omitted, it will be written to stdout.""")
217
218     @staticmethod
219     def run(args):
220         if args.output != '-':
221             raise NotImplementedError('Only output to stdout is currently implemented.')
222         return run_legacy_script('specialphrases.php', '--wiki-import', nominatim_env=args)
223
224
225 class UpdateReplication:
226     """\
227     Update the database using an online replication service.
228     """
229
230     @staticmethod
231     def add_args(parser):
232         group = parser.add_argument_group('Arguments for initialisation')
233         group.add_argument('--init', action='store_true',
234                            help='Initialise the update process')
235         group.add_argument('--no-update-functions', dest='update_functions',
236                            action='store_false',
237                            help="""Do not update the trigger function to
238                                    support differential updates.""")
239         group = parser.add_argument_group('Arguments for updates')
240         group.add_argument('--check-for-updates', action='store_true',
241                            help='Check if new updates are available and exit')
242         group.add_argument('--once', action='store_true',
243                            help="""Download and apply updates only once. When
244                                    not set, updates are continuously applied""")
245         group.add_argument('--no-index', action='store_false', dest='do_index',
246                            help="""Do not index the new data. Only applicable
247                                    together with --once""")
248         group.add_argument('--osm2pgsql-cache', metavar='SIZE', type=int,
249                            help='Size of cache to be used by osm2pgsql (in MB)')
250
251     @staticmethod
252     def _init_replication(args):
253         from .tools import replication, refresh
254
255         LOG.warning("Initialising replication updates")
256         conn = connect(args.config.get_libpq_dsn())
257         replication.init_replication(conn, base_url=args.config.REPLICATION_URL)
258         if args.update_functions:
259             LOG.warning("Create functions")
260             refresh.create_functions(conn, args.config, args.data_dir,
261                                      True, False)
262         conn.close()
263         return 0
264
265
266     @staticmethod
267     def _check_for_updates(args):
268         from .tools import replication
269
270         conn = connect(args.config.get_libpq_dsn())
271         ret = replication.check_for_updates(conn, base_url=args.config.REPLICATION_URL)
272         conn.close()
273         return ret
274
275
276     @staticmethod
277     def _update(args):
278         from .tools import replication
279         from .indexer.indexer import Indexer
280
281         params = _osm2pgsql_options_from_args(args, 2000, 1)
282         params.update(base_url=args.config.REPLICATION_URL,
283                       update_interval=args.config.get_int('REPLICATION_UPDATE_INTERVAL'),
284                       import_file=args.project_dir / 'osmosischange.osc',
285                       max_diff_size=args.config.get_int('REPLICATION_MAX_DIFF'),
286                       indexed_only=not args.once)
287
288         # Sanity check to not overwhelm the Geofabrik servers.
289         if 'download.geofabrik.de'in params['base_url']\
290            and params['update_interval'] < 86400:
291             LOG.fatal("Update interval too low for download.geofabrik.de.\n"
292                       "Please check install documentation "
293                       "(https://nominatim.org/release-docs/latest/admin/Import-and-Update#"
294                       "setting-up-the-update-process).")
295             raise RuntimeError("Invalid replication update interval setting.")
296
297         if not args.once:
298             if not args.do_index:
299                 LOG.fatal("Indexing cannot be disabled when running updates continuously.")
300                 raise RuntimeError("Bad arguments.")
301             recheck_interval = args.config.get_int('REPLICATION_RECHECK_INTERVAL')
302
303         while True:
304             conn = connect(args.config.get_libpq_dsn())
305             start = dt.datetime.now(dt.timezone.utc)
306             state = replication.update(conn, params)
307             status.log_status(conn, start, 'import')
308             conn.close()
309
310             if state is not replication.UpdateState.NO_CHANGES and args.do_index:
311                 start = dt.datetime.now(dt.timezone.utc)
312                 indexer = Indexer(args.config.get_libpq_dsn(),
313                                   args.threads or 1)
314                 indexer.index_boundaries(0, 30)
315                 indexer.index_by_rank(0, 30)
316
317                 conn = connect(args.config.get_libpq_dsn())
318                 status.set_indexed(conn, True)
319                 status.log_status(conn, start, 'index')
320                 conn.close()
321
322             if args.once:
323                 break
324
325             if state is replication.UpdateState.NO_CHANGES:
326                 LOG.warning("No new changes. Sleeping for %d sec.", recheck_interval)
327                 time.sleep(recheck_interval)
328
329         return state.value
330
331     @staticmethod
332     def run(args):
333         try:
334             import osmium # pylint: disable=W0611
335         except ModuleNotFoundError:
336             LOG.fatal("pyosmium not installed. Replication functions not available.\n"
337                       "To install pyosmium via pip: pip3 install osmium")
338             return 1
339
340         if args.init:
341             return UpdateReplication._init_replication(args)
342
343         if args.check_for_updates:
344             return UpdateReplication._check_for_updates(args)
345
346         return UpdateReplication._update(args)
347
348 class UpdateAddData:
349     """\
350     Add additional data from a file or an online source.
351
352     Data is only imported, not indexed. You need to call `nominatim-update index`
353     to complete the process.
354     """
355
356     @staticmethod
357     def add_args(parser):
358         group_name = parser.add_argument_group('Source')
359         group = group_name.add_mutually_exclusive_group(required=True)
360         group.add_argument('--file', metavar='FILE',
361                            help='Import data from an OSM file')
362         group.add_argument('--diff', metavar='FILE',
363                            help='Import data from an OSM diff file')
364         group.add_argument('--node', metavar='ID', type=int,
365                            help='Import a single node from the API')
366         group.add_argument('--way', metavar='ID', type=int,
367                            help='Import a single way from the API')
368         group.add_argument('--relation', metavar='ID', type=int,
369                            help='Import a single relation from the API')
370         group.add_argument('--tiger-data', metavar='DIR',
371                            help='Add housenumbers from the US TIGER census database.')
372         group = parser.add_argument_group('Extra arguments')
373         group.add_argument('--use-main-api', action='store_true',
374                            help='Use OSM API instead of Overpass to download objects')
375
376     @staticmethod
377     def run(args):
378         if args.tiger_data:
379             os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
380             return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
381
382         params = ['update.php']
383         if args.file:
384             params.extend(('--import-file', args.file))
385         elif args.diff:
386             params.extend(('--import-diff', args.diff))
387         elif args.node:
388             params.extend(('--import-node', args.node))
389         elif args.way:
390             params.extend(('--import-way', args.way))
391         elif args.relation:
392             params.extend(('--import-relation', args.relation))
393         if args.use_main_api:
394             params.append('--use-main-api')
395         return run_legacy_script(*params, nominatim_env=args)
396
397
398 class UpdateIndex:
399     """\
400     Reindex all new and modified data.
401     """
402
403     @staticmethod
404     def add_args(parser):
405         group = parser.add_argument_group('Filter arguments')
406         group.add_argument('--boundaries-only', action='store_true',
407                            help="""Index only administrative boundaries.""")
408         group.add_argument('--no-boundaries', action='store_true',
409                            help="""Index everything except administrative boundaries.""")
410         group.add_argument('--minrank', '-r', type=int, metavar='RANK', default=0,
411                            help='Minimum/starting rank')
412         group.add_argument('--maxrank', '-R', type=int, metavar='RANK', default=30,
413                            help='Maximum/finishing rank')
414
415     @staticmethod
416     def run(args):
417         from .indexer.indexer import Indexer
418
419         indexer = Indexer(args.config.get_libpq_dsn(),
420                           args.threads or _num_system_cpus() or 1)
421
422         if not args.no_boundaries:
423             indexer.index_boundaries(args.minrank, args.maxrank)
424         if not args.boundaries_only:
425             indexer.index_by_rank(args.minrank, args.maxrank)
426
427         if not args.no_boundaries and not args.boundaries_only \
428            and args.minrank == 0 and args.maxrank == 30:
429             conn = connect(args.config.get_libpq_dsn())
430             status.set_indexed(conn, True)
431             conn.close()
432
433         return 0
434
435
436 class UpdateRefresh:
437     """\
438     Recompute auxiliary data used by the indexing process.
439
440     These functions must not be run in parallel with other update commands.
441     """
442
443     @staticmethod
444     def add_args(parser):
445         group = parser.add_argument_group('Data arguments')
446         group.add_argument('--postcodes', action='store_true',
447                            help='Update postcode centroid table')
448         group.add_argument('--word-counts', action='store_true',
449                            help='Compute frequency of full-word search terms')
450         group.add_argument('--address-levels', action='store_true',
451                            help='Reimport address level configuration')
452         group.add_argument('--functions', action='store_true',
453                            help='Update the PL/pgSQL functions in the database')
454         group.add_argument('--wiki-data', action='store_true',
455                            help='Update Wikipedia/data importance numbers.')
456         group.add_argument('--importance', action='store_true',
457                            help='Recompute place importances (expensive!)')
458         group.add_argument('--website', action='store_true',
459                            help='Refresh the directory that serves the scripts for the web API')
460         group = parser.add_argument_group('Arguments for function refresh')
461         group.add_argument('--no-diff-updates', action='store_false', dest='diffs',
462                            help='Do not enable code for propagating updates')
463         group.add_argument('--enable-debug-statements', action='store_true',
464                            help='Enable debug warning statements in functions')
465
466     @staticmethod
467     def run(args):
468         from .tools import refresh
469
470         if args.postcodes:
471             LOG.warning("Update postcodes centroid")
472             conn = connect(args.config.get_libpq_dsn())
473             refresh.update_postcodes(conn, args.data_dir)
474             conn.close()
475
476         if args.word_counts:
477             LOG.warning('Recompute frequency of full-word search terms')
478             conn = connect(args.config.get_libpq_dsn())
479             refresh.recompute_word_counts(conn, args.data_dir)
480             conn.close()
481
482         if args.address_levels:
483             cfg = Path(args.config.ADDRESS_LEVEL_CONFIG)
484             LOG.warning('Updating address levels from %s', cfg)
485             conn = connect(args.config.get_libpq_dsn())
486             refresh.load_address_levels_from_file(conn, cfg)
487             conn.close()
488
489         if args.functions:
490             LOG.warning('Create functions')
491             conn = connect(args.config.get_libpq_dsn())
492             refresh.create_functions(conn, args.config, args.data_dir,
493                                      args.diffs, args.enable_debug_statements)
494             conn.close()
495
496         if args.wiki_data:
497             run_legacy_script('setup.php', '--import-wikipedia-articles',
498                               nominatim_env=args, throw_on_fail=True)
499         # Attention: importance MUST come after wiki data import.
500         if args.importance:
501             run_legacy_script('update.php', '--recompute-importance',
502                               nominatim_env=args, throw_on_fail=True)
503         if args.website:
504             run_legacy_script('setup.php', '--setup-website',
505                               nominatim_env=args, throw_on_fail=True)
506
507         return 0
508
509
510 class AdminCheckDatabase:
511     """\
512     Check that the database is complete and operational.
513     """
514
515     @staticmethod
516     def add_args(parser):
517         pass # No options
518
519     @staticmethod
520     def run(args):
521         return run_legacy_script('check_import_finished.php', nominatim_env=args)
522
523
524 class AdminWarm:
525     """\
526     Warm database caches for search and reverse queries.
527     """
528
529     @staticmethod
530     def add_args(parser):
531         group = parser.add_argument_group('Target arguments')
532         group.add_argument('--search-only', action='store_const', dest='target',
533                            const='search',
534                            help="Only pre-warm tables for search queries")
535         group.add_argument('--reverse-only', action='store_const', dest='target',
536                            const='reverse',
537                            help="Only pre-warm tables for reverse queries")
538
539     @staticmethod
540     def run(args):
541         params = ['warm.php']
542         if args.target == 'reverse':
543             params.append('--reverse-only')
544         if args.target == 'search':
545             params.append('--search-only')
546         return run_legacy_script(*params, nominatim_env=args)
547
548
549 class QueryExport:
550     """\
551     Export addresses as CSV file from the database.
552     """
553
554     @staticmethod
555     def add_args(parser):
556         group = parser.add_argument_group('Output arguments')
557         group.add_argument('--output-type', default='street',
558                            choices=('continent', 'country', 'state', 'county',
559                                     'city', 'suburb', 'street', 'path'),
560                            help='Type of places to output (default: street)')
561         group.add_argument('--output-format',
562                            default='street;suburb;city;county;state;country',
563                            help="""Semicolon-separated list of address types
564                                    (see --output-type). Multiple ranks can be
565                                    merged into one column by simply using a
566                                    comma-separated list.""")
567         group.add_argument('--output-all-postcodes', action='store_true',
568                            help="""List all postcodes for address instead of
569                                    just the most likely one""")
570         group.add_argument('--language',
571                            help="""Preferred language for output
572                                    (use local name, if omitted)""")
573         group = parser.add_argument_group('Filter arguments')
574         group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
575                            help='Export only objects within country')
576         group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
577                            help='Export only children of this OSM node')
578         group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
579                            help='Export only children of this OSM way')
580         group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
581                            help='Export only children of this OSM relation')
582
583
584     @staticmethod
585     def run(args):
586         params = ['export.php',
587                   '--output-type', args.output_type,
588                   '--output-format', args.output_format]
589         if args.output_all_postcodes:
590             params.append('--output-all-postcodes')
591         if args.language:
592             params.extend(('--language', args.language))
593         if args.restrict_to_country:
594             params.extend(('--restrict-to-country', args.restrict_to_country))
595         if args.restrict_to_osm_node:
596             params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
597         if args.restrict_to_osm_way:
598             params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
599         if args.restrict_to_osm_relation:
600             params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
601
602         return run_legacy_script(*params, nominatim_env=args)
603
604 STRUCTURED_QUERY = (
605     ('street', 'housenumber and street'),
606     ('city', 'city, town or village'),
607     ('county', 'county'),
608     ('state', 'state'),
609     ('country', 'country'),
610     ('postalcode', 'postcode')
611 )
612
613 EXTRADATA_PARAMS = (
614     ('addressdetails', 'Include a breakdown of the address into elements.'),
615     ('extratags', """Include additional information if available
616                      (e.g. wikipedia link, opening hours)."""),
617     ('namedetails', 'Include a list of alternative names.')
618 )
619
620 DETAILS_SWITCHES = (
621     ('addressdetails', 'Include a breakdown of the address into elements.'),
622     ('keywords', 'Include a list of name keywords and address keywords.'),
623     ('linkedplaces', 'Include a details of places that are linked with this one.'),
624     ('hierarchy', 'Include details of places lower in the address hierarchy.'),
625     ('group_hierarchy', 'Group the places by type.'),
626     ('polygon_geojson', 'Include geometry of result.')
627 )
628
629 def _add_api_output_arguments(parser):
630     group = parser.add_argument_group('Output arguments')
631     group.add_argument('--format', default='jsonv2',
632                        choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
633                        help='Format of result')
634     for name, desc in EXTRADATA_PARAMS:
635         group.add_argument('--' + name, action='store_true', help=desc)
636
637     group.add_argument('--lang', '--accept-language', metavar='LANGS',
638                        help='Preferred language order for presenting search results')
639     group.add_argument('--polygon-output',
640                        choices=['geojson', 'kml', 'svg', 'text'],
641                        help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
642     group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
643                        help="""Simplify output geometry.
644                                Parameter is difference tolerance in degrees.""")
645
646
647 class APISearch:
648     """\
649     Execute API search query.
650     """
651
652     @staticmethod
653     def add_args(parser):
654         group = parser.add_argument_group('Query arguments')
655         group.add_argument('--query',
656                            help='Free-form query string')
657         for name, desc in STRUCTURED_QUERY:
658             group.add_argument('--' + name, help='Structured query: ' + desc)
659
660         _add_api_output_arguments(parser)
661
662         group = parser.add_argument_group('Result limitation')
663         group.add_argument('--countrycodes', metavar='CC,..',
664                            help='Limit search results to one or more countries.')
665         group.add_argument('--exclude_place_ids', metavar='ID,..',
666                            help='List of search object to be excluded')
667         group.add_argument('--limit', type=int,
668                            help='Limit the number of returned results')
669         group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
670                            help='Preferred area to find search results')
671         group.add_argument('--bounded', action='store_true',
672                            help='Strictly restrict results to viewbox area')
673
674         group = parser.add_argument_group('Other arguments')
675         group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
676                            help='Do not remove duplicates from the result list')
677
678
679     @staticmethod
680     def run(args):
681         if args.query:
682             params = dict(q=args.query)
683         else:
684             params = {k : getattr(args, k) for k, _ in STRUCTURED_QUERY if getattr(args, k)}
685
686         for param, _ in EXTRADATA_PARAMS:
687             if getattr(args, param):
688                 params[param] = '1'
689         for param in ('format', 'countrycodes', 'exclude_place_ids', 'limit', 'viewbox'):
690             if getattr(args, param):
691                 params[param] = getattr(args, param)
692         if args.lang:
693             params['accept-language'] = args.lang
694         if args.polygon_output:
695             params['polygon_' + args.polygon_output] = '1'
696         if args.polygon_threshold:
697             params['polygon_threshold'] = args.polygon_threshold
698         if args.bounded:
699             params['bounded'] = '1'
700         if not args.dedupe:
701             params['dedupe'] = '0'
702
703         return run_api_script('search', args.project_dir,
704                               phpcgi_bin=args.phpcgi_path, params=params)
705
706 class APIReverse:
707     """\
708     Execute API reverse query.
709     """
710
711     @staticmethod
712     def add_args(parser):
713         group = parser.add_argument_group('Query arguments')
714         group.add_argument('--lat', type=float, required=True,
715                            help='Latitude of coordinate to look up (in WGS84)')
716         group.add_argument('--lon', type=float, required=True,
717                            help='Longitude of coordinate to look up (in WGS84)')
718         group.add_argument('--zoom', type=int,
719                            help='Level of detail required for the address')
720
721         _add_api_output_arguments(parser)
722
723
724     @staticmethod
725     def run(args):
726         params = dict(lat=args.lat, lon=args.lon)
727         if args.zoom is not None:
728             params['zoom'] = args.zoom
729
730         for param, _ in EXTRADATA_PARAMS:
731             if getattr(args, param):
732                 params[param] = '1'
733         if args.format:
734             params['format'] = args.format
735         if args.lang:
736             params['accept-language'] = args.lang
737         if args.polygon_output:
738             params['polygon_' + args.polygon_output] = '1'
739         if args.polygon_threshold:
740             params['polygon_threshold'] = args.polygon_threshold
741
742         return run_api_script('reverse', args.project_dir,
743                               phpcgi_bin=args.phpcgi_path, params=params)
744
745
746 class APILookup:
747     """\
748     Execute API reverse query.
749     """
750
751     @staticmethod
752     def add_args(parser):
753         group = parser.add_argument_group('Query arguments')
754         group.add_argument('--id', metavar='OSMID',
755                            action='append', required=True, dest='ids',
756                            help='OSM id to lookup in format <NRW><id> (may be repeated)')
757
758         _add_api_output_arguments(parser)
759
760
761     @staticmethod
762     def run(args):
763         params = dict(osm_ids=','.join(args.ids))
764
765         for param, _ in EXTRADATA_PARAMS:
766             if getattr(args, param):
767                 params[param] = '1'
768         if args.format:
769             params['format'] = args.format
770         if args.lang:
771             params['accept-language'] = args.lang
772         if args.polygon_output:
773             params['polygon_' + args.polygon_output] = '1'
774         if args.polygon_threshold:
775             params['polygon_threshold'] = args.polygon_threshold
776
777         return run_api_script('lookup', args.project_dir,
778                               phpcgi_bin=args.phpcgi_path, params=params)
779
780
781 class APIDetails:
782     """\
783     Execute API lookup query.
784     """
785
786     @staticmethod
787     def add_args(parser):
788         group = parser.add_argument_group('Query arguments')
789         objs = group.add_mutually_exclusive_group(required=True)
790         objs.add_argument('--node', '-n', type=int,
791                           help="Look up the OSM node with the given ID.")
792         objs.add_argument('--way', '-w', type=int,
793                           help="Look up the OSM way with the given ID.")
794         objs.add_argument('--relation', '-r', type=int,
795                           help="Look up the OSM relation with the given ID.")
796         objs.add_argument('--place_id', '-p', type=int,
797                           help='Database internal identifier of the OSM object to look up.')
798         group.add_argument('--class', dest='object_class',
799                            help="""Class type to disambiguated multiple entries
800                                    of the same object.""")
801
802         group = parser.add_argument_group('Output arguments')
803         for name, desc in DETAILS_SWITCHES:
804             group.add_argument('--' + name, action='store_true', help=desc)
805         group.add_argument('--lang', '--accept-language', metavar='LANGS',
806                            help='Preferred language order for presenting search results')
807
808     @staticmethod
809     def run(args):
810         if args.node:
811             params = dict(osmtype='N', osmid=args.node)
812         elif args.way:
813             params = dict(osmtype='W', osmid=args.node)
814         elif args.relation:
815             params = dict(osmtype='R', osmid=args.node)
816         else:
817             params = dict(place_id=args.place_id)
818         if args.object_class:
819             params['class'] = args.object_class
820         for name, _ in DETAILS_SWITCHES:
821             params[name] = '1' if getattr(args, name) else '0'
822
823         return run_api_script('details', args.project_dir,
824                               phpcgi_bin=args.phpcgi_path, params=params)
825
826
827 class APIStatus:
828     """\
829     Execute API status query.
830     """
831
832     @staticmethod
833     def add_args(parser):
834         group = parser.add_argument_group('API parameters')
835         group.add_argument('--format', default='text', choices=['text', 'json'],
836                            help='Format of result')
837
838     @staticmethod
839     def run(args):
840         return run_api_script('status', args.project_dir,
841                               phpcgi_bin=args.phpcgi_path,
842                               params=dict(format=args.format))
843
844
845 def nominatim(**kwargs):
846     """\
847     Command-line tools for importing, updating, administrating and
848     querying the Nominatim database.
849     """
850     parser = CommandlineParser('nominatim', nominatim.__doc__)
851
852     parser.add_subcommand('import', SetupAll)
853     parser.add_subcommand('freeze', SetupFreeze)
854     parser.add_subcommand('replication', UpdateReplication)
855
856     parser.add_subcommand('check-database', AdminCheckDatabase)
857     parser.add_subcommand('warm', AdminWarm)
858
859     parser.add_subcommand('special-phrases', SetupSpecialPhrases)
860
861     parser.add_subcommand('add-data', UpdateAddData)
862     parser.add_subcommand('index', UpdateIndex)
863     parser.add_subcommand('refresh', UpdateRefresh)
864
865     parser.add_subcommand('export', QueryExport)
866
867     if kwargs.get('phpcgi_path'):
868         parser.add_subcommand('search', APISearch)
869         parser.add_subcommand('reverse', APIReverse)
870         parser.add_subcommand('lookup', APILookup)
871         parser.add_subcommand('details', APIDetails)
872         parser.add_subcommand('status', APIStatus)
873     else:
874         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
875
876     return parser.run(**kwargs)