2 Command-line interface to the Nominatim functions for import, update,
 
   3 database administration and querying.
 
  12 from pathlib import Path
 
  14 from .config import Configuration
 
  15 from .tools.exec_utils import run_legacy_script, run_api_script, run_php_server
 
  16 from .db.connection import connect
 
  17 from .db import status
 
  18 from .errors import UsageError
 
  20 LOG = logging.getLogger()
 
  22 def _num_system_cpus():
 
  24         cpus = len(os.sched_getaffinity(0))
 
  25     except NotImplementedError:
 
  28     return cpus or os.cpu_count()
 
  31 class CommandlineParser:
 
  32     """ Wraps some of the common functions for parsing the command line
 
  33         and setting up subcommands.
 
  35     def __init__(self, prog, description):
 
  36         self.parser = argparse.ArgumentParser(
 
  38             description=description,
 
  39             formatter_class=argparse.RawDescriptionHelpFormatter)
 
  41         self.subs = self.parser.add_subparsers(title='available commands',
 
  44         # Arguments added to every sub-command
 
  45         self.default_args = argparse.ArgumentParser(add_help=False)
 
  46         group = self.default_args.add_argument_group('Default arguments')
 
  47         group.add_argument('-h', '--help', action='help',
 
  48                            help='Show this help message and exit')
 
  49         group.add_argument('-q', '--quiet', action='store_const', const=0,
 
  50                            dest='verbose', default=1,
 
  51                            help='Print only error messages')
 
  52         group.add_argument('-v', '--verbose', action='count', default=1,
 
  53                            help='Increase verboseness of output')
 
  54         group.add_argument('--project-dir', metavar='DIR', default='.',
 
  55                            help='Base directory of the Nominatim installation (default:.)')
 
  56         group.add_argument('-j', '--threads', metavar='NUM', type=int,
 
  57                            help='Number of parallel threads to use')
 
  60     def add_subcommand(self, name, cmd):
 
  61         """ Add a subcommand to the parser. The subcommand must be a class
 
  62             with a function add_args() that adds the parameters for the
 
  63             subcommand and a run() function that executes the command.
 
  65         parser = self.subs.add_parser(name, parents=[self.default_args],
 
  66                                       help=cmd.__doc__.split('\n', 1)[0],
 
  67                                       description=cmd.__doc__,
 
  68                                       formatter_class=argparse.RawDescriptionHelpFormatter,
 
  70         parser.set_defaults(command=cmd)
 
  73     def run(self, **kwargs):
 
  74         """ Parse the command line arguments of the program and execute the
 
  75             appropriate subcommand.
 
  77         args = self.parser.parse_args(args=kwargs.get('cli_args'))
 
  79         if args.subcommand is None:
 
  80             self.parser.print_help()
 
  83         for arg in ('module_dir', 'osm2pgsql_path', 'phplib_dir', 'data_dir', 'phpcgi_path'):
 
  84             setattr(args, arg, Path(kwargs[arg]))
 
  85         args.project_dir = Path(args.project_dir).resolve()
 
  87         logging.basicConfig(stream=sys.stderr,
 
  88                             format='%(asctime)s: %(message)s',
 
  89                             datefmt='%Y-%m-%d %H:%M:%S',
 
  90                             level=max(4 - args.verbose, 1) * 10)
 
  92         args.config = Configuration(args.project_dir, args.data_dir / 'settings')
 
  94         log = logging.getLogger()
 
  95         log.warning('Using project directory: %s', str(args.project_dir))
 
  98             return args.command.run(args)
 
  99         except UsageError as exception:
 
 100             if log.isEnabledFor(logging.DEBUG):
 
 101                 raise # use Python's exception printing
 
 102             log.fatal('FATAL: %s', exception)
 
 104         # If we get here, then execution has failed in some way.
 
 108 def _osm2pgsql_options_from_args(args, default_cache, default_threads):
 
 109     """ Set up the stanadrd osm2pgsql from the command line arguments.
 
 111     return dict(osm2pgsql=args.osm2pgsql_path,
 
 112                 osm2pgsql_cache=args.osm2pgsql_cache or default_cache,
 
 113                 osm2pgsql_style=args.config.get_import_style_file(),
 
 114                 threads=args.threads or default_threads,
 
 115                 dsn=args.config.get_libpq_dsn(),
 
 116                 flatnode_file=args.config.FLATNODE_FILE)
 
 118 ##### Subcommand classes
 
 120 # Each class needs to implement two functions: add_args() adds the CLI parameters
 
 121 # for the subfunction, run() executes the subcommand.
 
 123 # The class documentation doubles as the help text for the command. The
 
 124 # first line is also used in the summary when calling the program without
 
 127 # No need to document the functions each time.
 
 128 # pylint: disable=C0111
 
 129 # Using non-top-level imports to make pyosmium optional for replication only.
 
 130 # pylint: disable=E0012,C0415
 
 135     Create a new Nominatim database from an OSM file.
 
 139     def add_args(parser):
 
 140         group_name = parser.add_argument_group('Required arguments')
 
 141         group = group_name.add_mutually_exclusive_group(required=True)
 
 142         group.add_argument('--osm-file',
 
 143                            help='OSM file to be imported.')
 
 144         group.add_argument('--continue', dest='continue_at',
 
 145                            choices=['load-data', 'indexing', 'db-postprocess'],
 
 146                            help='Continue an import that was interrupted')
 
 147         group = parser.add_argument_group('Optional arguments')
 
 148         group.add_argument('--osm2pgsql-cache', metavar='SIZE', type=int,
 
 149                            help='Size of cache to be used by osm2pgsql (in MB)')
 
 150         group.add_argument('--reverse-only', action='store_true',
 
 151                            help='Do not create tables and indexes for searching')
 
 152         group.add_argument('--enable-debug-statements', action='store_true',
 
 153                            help='Include debug warning statements in SQL code')
 
 154         group.add_argument('--no-partitions', action='store_true',
 
 155                            help="""Do not partition search indices
 
 156                                    (speeds up import of single country extracts)""")
 
 157         group.add_argument('--no-updates', action='store_true',
 
 158                            help="""Do not keep tables that are only needed for
 
 159                                    updating the database later""")
 
 160         group = parser.add_argument_group('Expert options')
 
 161         group.add_argument('--ignore-errors', action='store_true',
 
 162                            help='Continue import even when errors in SQL are present')
 
 163         group.add_argument('--index-noanalyse', action='store_true',
 
 164                            help='Do not perform analyse operations during index')
 
 169         params = ['setup.php']
 
 171             params.extend(('--all', '--osm-file', args.osm_file))
 
 173             if args.continue_at == 'load-data':
 
 174                 params.append('--load-data')
 
 175             if args.continue_at in ('load-data', 'indexing'):
 
 176                 params.append('--index')
 
 177             params.extend(('--create-search-indices', '--create-country-names',
 
 179         if args.osm2pgsql_cache:
 
 180             params.extend(('--osm2pgsql-cache', args.osm2pgsql_cache))
 
 181         if args.reverse_only:
 
 182             params.append('--reverse-only')
 
 183         if args.enable_debug_statements:
 
 184             params.append('--enable-debug-statements')
 
 185         if args.no_partitions:
 
 186             params.append('--no-partitions')
 
 188             params.append('--drop')
 
 189         if args.ignore_errors:
 
 190             params.append('--ignore-errors')
 
 191         if args.index_noanalyse:
 
 192             params.append('--index-noanalyse')
 
 194         return run_legacy_script(*params, nominatim_env=args)
 
 199     Make database read-only.
 
 201     About half of data in the Nominatim database is kept only to be able to
 
 202     keep the data up-to-date with new changes made in OpenStreetMap. This
 
 203     command drops all this data and only keeps the part needed for geocoding
 
 206     This command has the same effect as the `--no-updates` option for imports.
 
 210     def add_args(parser):
 
 215         return run_legacy_script('setup.php', '--drop', nominatim_env=args)
 
 218 class SetupSpecialPhrases:
 
 220     Maintain special phrases.
 
 224     def add_args(parser):
 
 225         group = parser.add_argument_group('Input arguments')
 
 226         group.add_argument('--from-wiki', action='store_true',
 
 227                            help='Pull special phrases from the OSM wiki.')
 
 228         group = parser.add_argument_group('Output arguments')
 
 229         group.add_argument('-o', '--output', default='-',
 
 230                            help="""File to write the preprocessed phrases to.
 
 231                                    If omitted, it will be written to stdout.""")
 
 235         if args.output != '-':
 
 236             raise NotImplementedError('Only output to stdout is currently implemented.')
 
 237         return run_legacy_script('specialphrases.php', '--wiki-import', nominatim_env=args)
 
 240 class UpdateReplication:
 
 242     Update the database using an online replication service.
 
 246     def add_args(parser):
 
 247         group = parser.add_argument_group('Arguments for initialisation')
 
 248         group.add_argument('--init', action='store_true',
 
 249                            help='Initialise the update process')
 
 250         group.add_argument('--no-update-functions', dest='update_functions',
 
 251                            action='store_false',
 
 252                            help="""Do not update the trigger function to
 
 253                                    support differential updates.""")
 
 254         group = parser.add_argument_group('Arguments for updates')
 
 255         group.add_argument('--check-for-updates', action='store_true',
 
 256                            help='Check if new updates are available and exit')
 
 257         group.add_argument('--once', action='store_true',
 
 258                            help="""Download and apply updates only once. When
 
 259                                    not set, updates are continuously applied""")
 
 260         group.add_argument('--no-index', action='store_false', dest='do_index',
 
 261                            help="""Do not index the new data. Only applicable
 
 262                                    together with --once""")
 
 263         group.add_argument('--osm2pgsql-cache', metavar='SIZE', type=int,
 
 264                            help='Size of cache to be used by osm2pgsql (in MB)')
 
 265         group = parser.add_argument_group('Download parameters')
 
 266         group.add_argument('--socket-timeout', dest='socket_timeout', type=int, default=60,
 
 267                            help='Set timeout for file downloads.')
 
 270     def _init_replication(args):
 
 271         from .tools import replication, refresh
 
 273         socket.setdefaulttimeout(args.socket_timeout)
 
 275         LOG.warning("Initialising replication updates")
 
 276         conn = connect(args.config.get_libpq_dsn())
 
 277         replication.init_replication(conn, base_url=args.config.REPLICATION_URL)
 
 278         if args.update_functions:
 
 279             LOG.warning("Create functions")
 
 280             refresh.create_functions(conn, args.config, args.data_dir,
 
 287     def _check_for_updates(args):
 
 288         from .tools import replication
 
 290         conn = connect(args.config.get_libpq_dsn())
 
 291         ret = replication.check_for_updates(conn, base_url=args.config.REPLICATION_URL)
 
 296     def _report_update(batchdate, start_import, start_index):
 
 297         def round_time(delta):
 
 298             return dt.timedelta(seconds=int(delta.total_seconds()))
 
 300         end = dt.datetime.now(dt.timezone.utc)
 
 301         LOG.warning("Update completed. Import: %s. %sTotal: %s. Remaining backlog: %s.",
 
 302                     round_time((start_index or end) - start_import),
 
 303                     "Indexing: {} ".format(round_time(end - start_index))
 
 304                     if start_index else '',
 
 305                     round_time(end - start_import),
 
 306                     round_time(end - batchdate))
 
 310         from .tools import replication
 
 311         from .indexer.indexer import Indexer
 
 313         params = _osm2pgsql_options_from_args(args, 2000, 1)
 
 314         params.update(base_url=args.config.REPLICATION_URL,
 
 315                       update_interval=args.config.get_int('REPLICATION_UPDATE_INTERVAL'),
 
 316                       import_file=args.project_dir / 'osmosischange.osc',
 
 317                       max_diff_size=args.config.get_int('REPLICATION_MAX_DIFF'),
 
 318                       indexed_only=not args.once)
 
 320         # Sanity check to not overwhelm the Geofabrik servers.
 
 321         if 'download.geofabrik.de'in params['base_url']\
 
 322            and params['update_interval'] < 86400:
 
 323             LOG.fatal("Update interval too low for download.geofabrik.de.\n"
 
 324                       "Please check install documentation "
 
 325                       "(https://nominatim.org/release-docs/latest/admin/Import-and-Update#"
 
 326                       "setting-up-the-update-process).")
 
 327             raise UsageError("Invalid replication update interval setting.")
 
 330             if not args.do_index:
 
 331                 LOG.fatal("Indexing cannot be disabled when running updates continuously.")
 
 332                 raise UsageError("Bad argument '--no-index'.")
 
 333             recheck_interval = args.config.get_int('REPLICATION_RECHECK_INTERVAL')
 
 336             conn = connect(args.config.get_libpq_dsn())
 
 337             start = dt.datetime.now(dt.timezone.utc)
 
 338             state = replication.update(conn, params)
 
 339             if state is not replication.UpdateState.NO_CHANGES:
 
 340                 status.log_status(conn, start, 'import')
 
 341             batchdate, _, _ = status.get_status(conn)
 
 344             if state is not replication.UpdateState.NO_CHANGES and args.do_index:
 
 345                 index_start = dt.datetime.now(dt.timezone.utc)
 
 346                 indexer = Indexer(args.config.get_libpq_dsn(),
 
 348                 indexer.index_boundaries(0, 30)
 
 349                 indexer.index_by_rank(0, 30)
 
 351                 conn = connect(args.config.get_libpq_dsn())
 
 352                 status.set_indexed(conn, True)
 
 353                 status.log_status(conn, index_start, 'index')
 
 358             if LOG.isEnabledFor(logging.WARNING):
 
 359                 UpdateReplication._report_update(batchdate, start, index_start)
 
 364             if state is replication.UpdateState.NO_CHANGES:
 
 365                 LOG.warning("No new changes. Sleeping for %d sec.", recheck_interval)
 
 366                 time.sleep(recheck_interval)
 
 373             import osmium # pylint: disable=W0611
 
 374         except ModuleNotFoundError:
 
 375             LOG.fatal("pyosmium not installed. Replication functions not available.\n"
 
 376                       "To install pyosmium via pip: pip3 install osmium")
 
 380             return UpdateReplication._init_replication(args)
 
 382         if args.check_for_updates:
 
 383             return UpdateReplication._check_for_updates(args)
 
 385         return UpdateReplication._update(args)
 
 389     Add additional data from a file or an online source.
 
 391     Data is only imported, not indexed. You need to call `nominatim-update index`
 
 392     to complete the process.
 
 396     def add_args(parser):
 
 397         group_name = parser.add_argument_group('Source')
 
 398         group = group_name.add_mutually_exclusive_group(required=True)
 
 399         group.add_argument('--file', metavar='FILE',
 
 400                            help='Import data from an OSM file')
 
 401         group.add_argument('--diff', metavar='FILE',
 
 402                            help='Import data from an OSM diff file')
 
 403         group.add_argument('--node', metavar='ID', type=int,
 
 404                            help='Import a single node from the API')
 
 405         group.add_argument('--way', metavar='ID', type=int,
 
 406                            help='Import a single way from the API')
 
 407         group.add_argument('--relation', metavar='ID', type=int,
 
 408                            help='Import a single relation from the API')
 
 409         group.add_argument('--tiger-data', metavar='DIR',
 
 410                            help='Add housenumbers from the US TIGER census database.')
 
 411         group = parser.add_argument_group('Extra arguments')
 
 412         group.add_argument('--use-main-api', action='store_true',
 
 413                            help='Use OSM API instead of Overpass to download objects')
 
 418             os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
 
 419             return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
 
 421         params = ['update.php']
 
 423             params.extend(('--import-file', args.file))
 
 425             params.extend(('--import-diff', args.diff))
 
 427             params.extend(('--import-node', args.node))
 
 429             params.extend(('--import-way', args.way))
 
 431             params.extend(('--import-relation', args.relation))
 
 432         if args.use_main_api:
 
 433             params.append('--use-main-api')
 
 434         return run_legacy_script(*params, nominatim_env=args)
 
 439     Reindex all new and modified data.
 
 443     def add_args(parser):
 
 444         group = parser.add_argument_group('Filter arguments')
 
 445         group.add_argument('--boundaries-only', action='store_true',
 
 446                            help="""Index only administrative boundaries.""")
 
 447         group.add_argument('--no-boundaries', action='store_true',
 
 448                            help="""Index everything except administrative boundaries.""")
 
 449         group.add_argument('--minrank', '-r', type=int, metavar='RANK', default=0,
 
 450                            help='Minimum/starting rank')
 
 451         group.add_argument('--maxrank', '-R', type=int, metavar='RANK', default=30,
 
 452                            help='Maximum/finishing rank')
 
 456         from .indexer.indexer import Indexer
 
 458         indexer = Indexer(args.config.get_libpq_dsn(),
 
 459                           args.threads or _num_system_cpus() or 1)
 
 461         if not args.no_boundaries:
 
 462             indexer.index_boundaries(args.minrank, args.maxrank)
 
 463         if not args.boundaries_only:
 
 464             indexer.index_by_rank(args.minrank, args.maxrank)
 
 466         if not args.no_boundaries and not args.boundaries_only \
 
 467            and args.minrank == 0 and args.maxrank == 30:
 
 468             conn = connect(args.config.get_libpq_dsn())
 
 469             status.set_indexed(conn, True)
 
 477     Recompute auxiliary data used by the indexing process.
 
 479     These functions must not be run in parallel with other update commands.
 
 483     def add_args(parser):
 
 484         group = parser.add_argument_group('Data arguments')
 
 485         group.add_argument('--postcodes', action='store_true',
 
 486                            help='Update postcode centroid table')
 
 487         group.add_argument('--word-counts', action='store_true',
 
 488                            help='Compute frequency of full-word search terms')
 
 489         group.add_argument('--address-levels', action='store_true',
 
 490                            help='Reimport address level configuration')
 
 491         group.add_argument('--functions', action='store_true',
 
 492                            help='Update the PL/pgSQL functions in the database')
 
 493         group.add_argument('--wiki-data', action='store_true',
 
 494                            help='Update Wikipedia/data importance numbers.')
 
 495         group.add_argument('--importance', action='store_true',
 
 496                            help='Recompute place importances (expensive!)')
 
 497         group.add_argument('--website', action='store_true',
 
 498                            help='Refresh the directory that serves the scripts for the web API')
 
 499         group = parser.add_argument_group('Arguments for function refresh')
 
 500         group.add_argument('--no-diff-updates', action='store_false', dest='diffs',
 
 501                            help='Do not enable code for propagating updates')
 
 502         group.add_argument('--enable-debug-statements', action='store_true',
 
 503                            help='Enable debug warning statements in functions')
 
 507         from .tools import refresh
 
 510             LOG.warning("Update postcodes centroid")
 
 511             conn = connect(args.config.get_libpq_dsn())
 
 512             refresh.update_postcodes(conn, args.data_dir)
 
 516             LOG.warning('Recompute frequency of full-word search terms')
 
 517             conn = connect(args.config.get_libpq_dsn())
 
 518             refresh.recompute_word_counts(conn, args.data_dir)
 
 521         if args.address_levels:
 
 522             cfg = Path(args.config.ADDRESS_LEVEL_CONFIG)
 
 523             LOG.warning('Updating address levels from %s', cfg)
 
 524             conn = connect(args.config.get_libpq_dsn())
 
 525             refresh.load_address_levels_from_file(conn, cfg)
 
 529             LOG.warning('Create functions')
 
 530             conn = connect(args.config.get_libpq_dsn())
 
 531             refresh.create_functions(conn, args.config, args.data_dir,
 
 532                                      args.diffs, args.enable_debug_statements)
 
 536             run_legacy_script('setup.php', '--import-wikipedia-articles',
 
 537                               nominatim_env=args, throw_on_fail=True)
 
 538         # Attention: importance MUST come after wiki data import.
 
 540             run_legacy_script('update.php', '--recompute-importance',
 
 541                               nominatim_env=args, throw_on_fail=True)
 
 543             run_legacy_script('setup.php', '--setup-website',
 
 544                               nominatim_env=args, throw_on_fail=True)
 
 549 class AdminCheckDatabase:
 
 551     Check that the database is complete and operational.
 
 555     def add_args(parser):
 
 560         return run_legacy_script('check_import_finished.php', nominatim_env=args)
 
 565     Warm database caches for search and reverse queries.
 
 569     def add_args(parser):
 
 570         group = parser.add_argument_group('Target arguments')
 
 571         group.add_argument('--search-only', action='store_const', dest='target',
 
 573                            help="Only pre-warm tables for search queries")
 
 574         group.add_argument('--reverse-only', action='store_const', dest='target',
 
 576                            help="Only pre-warm tables for reverse queries")
 
 580         params = ['warm.php']
 
 581         if args.target == 'reverse':
 
 582             params.append('--reverse-only')
 
 583         if args.target == 'search':
 
 584             params.append('--search-only')
 
 585         return run_legacy_script(*params, nominatim_env=args)
 
 590     Export addresses as CSV file from the database.
 
 594     def add_args(parser):
 
 595         group = parser.add_argument_group('Output arguments')
 
 596         group.add_argument('--output-type', default='street',
 
 597                            choices=('continent', 'country', 'state', 'county',
 
 598                                     'city', 'suburb', 'street', 'path'),
 
 599                            help='Type of places to output (default: street)')
 
 600         group.add_argument('--output-format',
 
 601                            default='street;suburb;city;county;state;country',
 
 602                            help="""Semicolon-separated list of address types
 
 603                                    (see --output-type). Multiple ranks can be
 
 604                                    merged into one column by simply using a
 
 605                                    comma-separated list.""")
 
 606         group.add_argument('--output-all-postcodes', action='store_true',
 
 607                            help="""List all postcodes for address instead of
 
 608                                    just the most likely one""")
 
 609         group.add_argument('--language',
 
 610                            help="""Preferred language for output
 
 611                                    (use local name, if omitted)""")
 
 612         group = parser.add_argument_group('Filter arguments')
 
 613         group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
 
 614                            help='Export only objects within country')
 
 615         group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
 
 616                            help='Export only children of this OSM node')
 
 617         group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
 
 618                            help='Export only children of this OSM way')
 
 619         group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
 
 620                            help='Export only children of this OSM relation')
 
 625         params = ['export.php',
 
 626                   '--output-type', args.output_type,
 
 627                   '--output-format', args.output_format]
 
 628         if args.output_all_postcodes:
 
 629             params.append('--output-all-postcodes')
 
 631             params.extend(('--language', args.language))
 
 632         if args.restrict_to_country:
 
 633             params.extend(('--restrict-to-country', args.restrict_to_country))
 
 634         if args.restrict_to_osm_node:
 
 635             params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
 
 636         if args.restrict_to_osm_way:
 
 637             params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
 
 638         if args.restrict_to_osm_relation:
 
 639             params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
 
 641         return run_legacy_script(*params, nominatim_env=args)
 
 646     Start a simple web server for serving the API.
 
 648     This command starts the built-in PHP webserver to serve the website
 
 649     from the current project directory. This webserver is only suitable
 
 650     for testing and develop. Do not use it in production setups!
 
 652     By the default, the webserver can be accessed at: http://127.0.0.1:8088
 
 656     def add_args(parser):
 
 657         group = parser.add_argument_group('Server arguments')
 
 658         group.add_argument('--server', default='127.0.0.1:8088',
 
 659                            help='The address the server will listen to.')
 
 663         run_php_server(args.server, args.project_dir / 'website')
 
 666     ('street', 'housenumber and street'),
 
 667     ('city', 'city, town or village'),
 
 668     ('county', 'county'),
 
 670     ('country', 'country'),
 
 671     ('postalcode', 'postcode')
 
 675     ('addressdetails', 'Include a breakdown of the address into elements.'),
 
 676     ('extratags', """Include additional information if available
 
 677                      (e.g. wikipedia link, opening hours)."""),
 
 678     ('namedetails', 'Include a list of alternative names.')
 
 682     ('addressdetails', 'Include a breakdown of the address into elements.'),
 
 683     ('keywords', 'Include a list of name keywords and address keywords.'),
 
 684     ('linkedplaces', 'Include a details of places that are linked with this one.'),
 
 685     ('hierarchy', 'Include details of places lower in the address hierarchy.'),
 
 686     ('group_hierarchy', 'Group the places by type.'),
 
 687     ('polygon_geojson', 'Include geometry of result.')
 
 690 def _add_api_output_arguments(parser):
 
 691     group = parser.add_argument_group('Output arguments')
 
 692     group.add_argument('--format', default='jsonv2',
 
 693                        choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
 
 694                        help='Format of result')
 
 695     for name, desc in EXTRADATA_PARAMS:
 
 696         group.add_argument('--' + name, action='store_true', help=desc)
 
 698     group.add_argument('--lang', '--accept-language', metavar='LANGS',
 
 699                        help='Preferred language order for presenting search results')
 
 700     group.add_argument('--polygon-output',
 
 701                        choices=['geojson', 'kml', 'svg', 'text'],
 
 702                        help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
 
 703     group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
 
 704                        help="""Simplify output geometry.
 
 705                                Parameter is difference tolerance in degrees.""")
 
 710     Execute API search query.
 
 714     def add_args(parser):
 
 715         group = parser.add_argument_group('Query arguments')
 
 716         group.add_argument('--query',
 
 717                            help='Free-form query string')
 
 718         for name, desc in STRUCTURED_QUERY:
 
 719             group.add_argument('--' + name, help='Structured query: ' + desc)
 
 721         _add_api_output_arguments(parser)
 
 723         group = parser.add_argument_group('Result limitation')
 
 724         group.add_argument('--countrycodes', metavar='CC,..',
 
 725                            help='Limit search results to one or more countries.')
 
 726         group.add_argument('--exclude_place_ids', metavar='ID,..',
 
 727                            help='List of search object to be excluded')
 
 728         group.add_argument('--limit', type=int,
 
 729                            help='Limit the number of returned results')
 
 730         group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
 
 731                            help='Preferred area to find search results')
 
 732         group.add_argument('--bounded', action='store_true',
 
 733                            help='Strictly restrict results to viewbox area')
 
 735         group = parser.add_argument_group('Other arguments')
 
 736         group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
 
 737                            help='Do not remove duplicates from the result list')
 
 743             params = dict(q=args.query)
 
 745             params = {k : getattr(args, k) for k, _ in STRUCTURED_QUERY if getattr(args, k)}
 
 747         for param, _ in EXTRADATA_PARAMS:
 
 748             if getattr(args, param):
 
 750         for param in ('format', 'countrycodes', 'exclude_place_ids', 'limit', 'viewbox'):
 
 751             if getattr(args, param):
 
 752                 params[param] = getattr(args, param)
 
 754             params['accept-language'] = args.lang
 
 755         if args.polygon_output:
 
 756             params['polygon_' + args.polygon_output] = '1'
 
 757         if args.polygon_threshold:
 
 758             params['polygon_threshold'] = args.polygon_threshold
 
 760             params['bounded'] = '1'
 
 762             params['dedupe'] = '0'
 
 764         return run_api_script('search', args.project_dir,
 
 765                               phpcgi_bin=args.phpcgi_path, params=params)
 
 769     Execute API reverse query.
 
 773     def add_args(parser):
 
 774         group = parser.add_argument_group('Query arguments')
 
 775         group.add_argument('--lat', type=float, required=True,
 
 776                            help='Latitude of coordinate to look up (in WGS84)')
 
 777         group.add_argument('--lon', type=float, required=True,
 
 778                            help='Longitude of coordinate to look up (in WGS84)')
 
 779         group.add_argument('--zoom', type=int,
 
 780                            help='Level of detail required for the address')
 
 782         _add_api_output_arguments(parser)
 
 787         params = dict(lat=args.lat, lon=args.lon)
 
 788         if args.zoom is not None:
 
 789             params['zoom'] = args.zoom
 
 791         for param, _ in EXTRADATA_PARAMS:
 
 792             if getattr(args, param):
 
 795             params['format'] = args.format
 
 797             params['accept-language'] = args.lang
 
 798         if args.polygon_output:
 
 799             params['polygon_' + args.polygon_output] = '1'
 
 800         if args.polygon_threshold:
 
 801             params['polygon_threshold'] = args.polygon_threshold
 
 803         return run_api_script('reverse', args.project_dir,
 
 804                               phpcgi_bin=args.phpcgi_path, params=params)
 
 809     Execute API reverse query.
 
 813     def add_args(parser):
 
 814         group = parser.add_argument_group('Query arguments')
 
 815         group.add_argument('--id', metavar='OSMID',
 
 816                            action='append', required=True, dest='ids',
 
 817                            help='OSM id to lookup in format <NRW><id> (may be repeated)')
 
 819         _add_api_output_arguments(parser)
 
 824         params = dict(osm_ids=','.join(args.ids))
 
 826         for param, _ in EXTRADATA_PARAMS:
 
 827             if getattr(args, param):
 
 830             params['format'] = args.format
 
 832             params['accept-language'] = args.lang
 
 833         if args.polygon_output:
 
 834             params['polygon_' + args.polygon_output] = '1'
 
 835         if args.polygon_threshold:
 
 836             params['polygon_threshold'] = args.polygon_threshold
 
 838         return run_api_script('lookup', args.project_dir,
 
 839                               phpcgi_bin=args.phpcgi_path, params=params)
 
 844     Execute API lookup query.
 
 848     def add_args(parser):
 
 849         group = parser.add_argument_group('Query arguments')
 
 850         objs = group.add_mutually_exclusive_group(required=True)
 
 851         objs.add_argument('--node', '-n', type=int,
 
 852                           help="Look up the OSM node with the given ID.")
 
 853         objs.add_argument('--way', '-w', type=int,
 
 854                           help="Look up the OSM way with the given ID.")
 
 855         objs.add_argument('--relation', '-r', type=int,
 
 856                           help="Look up the OSM relation with the given ID.")
 
 857         objs.add_argument('--place_id', '-p', type=int,
 
 858                           help='Database internal identifier of the OSM object to look up.')
 
 859         group.add_argument('--class', dest='object_class',
 
 860                            help="""Class type to disambiguated multiple entries
 
 861                                    of the same object.""")
 
 863         group = parser.add_argument_group('Output arguments')
 
 864         for name, desc in DETAILS_SWITCHES:
 
 865             group.add_argument('--' + name, action='store_true', help=desc)
 
 866         group.add_argument('--lang', '--accept-language', metavar='LANGS',
 
 867                            help='Preferred language order for presenting search results')
 
 872             params = dict(osmtype='N', osmid=args.node)
 
 874             params = dict(osmtype='W', osmid=args.node)
 
 876             params = dict(osmtype='R', osmid=args.node)
 
 878             params = dict(place_id=args.place_id)
 
 879         if args.object_class:
 
 880             params['class'] = args.object_class
 
 881         for name, _ in DETAILS_SWITCHES:
 
 882             params[name] = '1' if getattr(args, name) else '0'
 
 884         return run_api_script('details', args.project_dir,
 
 885                               phpcgi_bin=args.phpcgi_path, params=params)
 
 890     Execute API status query.
 
 894     def add_args(parser):
 
 895         group = parser.add_argument_group('API parameters')
 
 896         group.add_argument('--format', default='text', choices=['text', 'json'],
 
 897                            help='Format of result')
 
 901         return run_api_script('status', args.project_dir,
 
 902                               phpcgi_bin=args.phpcgi_path,
 
 903                               params=dict(format=args.format))
 
 906 def nominatim(**kwargs):
 
 908     Command-line tools for importing, updating, administrating and
 
 909     querying the Nominatim database.
 
 911     parser = CommandlineParser('nominatim', nominatim.__doc__)
 
 913     parser.add_subcommand('import', SetupAll)
 
 914     parser.add_subcommand('freeze', SetupFreeze)
 
 915     parser.add_subcommand('replication', UpdateReplication)
 
 917     parser.add_subcommand('check-database', AdminCheckDatabase)
 
 918     parser.add_subcommand('warm', AdminWarm)
 
 920     parser.add_subcommand('special-phrases', SetupSpecialPhrases)
 
 922     parser.add_subcommand('add-data', UpdateAddData)
 
 923     parser.add_subcommand('index', UpdateIndex)
 
 924     parser.add_subcommand('refresh', UpdateRefresh)
 
 926     parser.add_subcommand('export', QueryExport)
 
 927     parser.add_subcommand('serve', AdminServe)
 
 929     if kwargs.get('phpcgi_path'):
 
 930         parser.add_subcommand('search', APISearch)
 
 931         parser.add_subcommand('reverse', APIReverse)
 
 932         parser.add_subcommand('lookup', APILookup)
 
 933         parser.add_subcommand('details', APIDetails)
 
 934         parser.add_subcommand('status', APIStatus)
 
 936         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
 
 938     return parser.run(**kwargs)