2 Command-line interface to the Nominatim functions for import, update,
3 database administration and querying.
9 from pathlib import Path
11 from .config import Configuration
12 from .tools.exec_utils import run_legacy_script, run_api_script
14 from .indexer.indexer import Indexer
16 def _num_system_cpus():
18 cpus = len(os.sched_getaffinity(0))
19 except NotImplementedError:
22 return cpus or os.cpu_count()
25 class CommandlineParser:
26 """ Wraps some of the common functions for parsing the command line
27 and setting up subcommands.
29 def __init__(self, prog, description):
30 self.parser = argparse.ArgumentParser(
32 description=description,
33 formatter_class=argparse.RawDescriptionHelpFormatter)
35 self.subs = self.parser.add_subparsers(title='available commands',
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')
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.
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,
64 parser.set_defaults(command=cmd)
67 def run(self, **kwargs):
68 """ Parse the command line arguments of the program and execute the
69 appropriate subcommand.
71 args = self.parser.parse_args()
73 if args.subcommand is None:
74 return self.parser.print_help()
76 for arg in ('module_dir', 'osm2pgsql_path', 'phplib_dir', 'data_dir', 'phpcgi_path'):
77 setattr(args, arg, Path(kwargs[arg]))
78 args.project_dir = Path(args.project_dir)
80 logging.basicConfig(stream=sys.stderr,
81 format='%(asctime)s: %(message)s',
82 datefmt='%Y-%m-%d %H:%M:%S',
83 level=max(4 - args.verbose, 1) * 10)
85 args.config = Configuration(args.project_dir, args.data_dir / 'settings')
87 return args.command.run(args)
89 ##### Subcommand classes
91 # Each class needs to implement two functions: add_args() adds the CLI parameters
92 # for the subfunction, run() executes the subcommand.
94 # The class documentation doubles as the help text for the command. The
95 # first line is also used in the summary when calling the program without
98 # No need to document the functions each time.
99 # pylint: disable=C0111
104 Create a new Nominatim database from an OSM file.
108 def add_args(parser):
109 group_name = parser.add_argument_group('Required arguments')
110 group = group_name.add_mutually_exclusive_group(required=True)
111 group.add_argument('--osm-file',
112 help='OSM file to be imported.')
113 group.add_argument('--continue', dest='continue_at',
114 choices=['load-data', 'indexing', 'db-postprocess'],
115 help='Continue an import that was interrupted')
116 group = parser.add_argument_group('Optional arguments')
117 group.add_argument('--osm2pgsql-cache', metavar='SIZE', type=int,
118 help='Size of cache to be used by osm2pgsql (in MB)')
119 group.add_argument('--reverse-only', action='store_true',
120 help='Do not create tables and indexes for searching')
121 group.add_argument('--enable-debug-statements', action='store_true',
122 help='Include debug warning statements in SQL code')
123 group.add_argument('--no-partitions', action='store_true',
124 help="""Do not partition search indices
125 (speeds up import of single country extracts)""")
126 group.add_argument('--no-updates', action='store_true',
127 help="""Do not keep tables that are only needed for
128 updating the database later""")
129 group = parser.add_argument_group('Expert options')
130 group.add_argument('--ignore-errors', action='store_true',
131 help='Continue import even when errors in SQL are present')
132 group.add_argument('--index-noanalyse', action='store_true',
133 help='Do not perform analyse operations during index')
138 params = ['setup.php']
140 params.extend(('--all', '--osm-file', args.osm_file))
142 if args.continue_at == 'load-data':
143 params.append('--load-data')
144 if args.continue_at in ('load-data', 'indexing'):
145 params.append('--index')
146 params.extend(('--create-search-indices', '--create-country-names',
148 if args.osm2pgsql_cache:
149 params.extend(('--osm2pgsql-cache', args.osm2pgsql_cache))
150 if args.reverse_only:
151 params.append('--reverse-only')
152 if args.enable_debug_statements:
153 params.append('--enable-debug-statements')
154 if args.no_partitions:
155 params.append('--no-partitions')
157 params.append('--drop')
158 if args.ignore_errors:
159 params.append('--ignore-errors')
160 if args.index_noanalyse:
161 params.append('--index-noanalyse')
163 return run_legacy_script(*params, nominatim_env=args)
168 Make database read-only.
170 About half of data in the Nominatim database is kept only to be able to
171 keep the data up-to-date with new changes made in OpenStreetMap. This
172 command drops all this data and only keeps the part needed for geocoding
175 This command has the same effect as the `--no-updates` option for imports.
179 def add_args(parser):
184 return run_legacy_script('setup.php', '--drop', nominatim_env=args)
187 class SetupSpecialPhrases:
189 Maintain special phrases.
193 def add_args(parser):
194 group = parser.add_argument_group('Input arguments')
195 group.add_argument('--from-wiki', action='store_true',
196 help='Pull special phrases from the OSM wiki.')
197 group = parser.add_argument_group('Output arguments')
198 group.add_argument('-o', '--output', default='-',
199 type=argparse.FileType('w', encoding='UTF-8'),
200 help="""File to write the preprocessed phrases to.
201 If omitted, it will be written to stdout.""")
205 if args.output.name != '<stdout>':
206 raise NotImplementedError('Only output to stdout is currently implemented.')
207 return run_legacy_script('specialphrases.php', '--wiki-import', nominatim_env=args)
210 class UpdateReplication:
212 Update the database using an online replication service.
216 def add_args(parser):
217 group = parser.add_argument_group('Arguments for initialisation')
218 group.add_argument('--init', action='store_true',
219 help='Initialise the update process')
220 group.add_argument('--no-update-functions', dest='update_functions',
221 action='store_false',
222 help="""Do not update the trigger function to
223 support differential updates.""")
224 group = parser.add_argument_group('Arguments for updates')
225 group.add_argument('--check-for-updates', action='store_true',
226 help='Check if new updates are available and exit')
227 group.add_argument('--once', action='store_true',
228 help="""Download and apply updates only once. When
229 not set, updates are continuously applied""")
230 group.add_argument('--no-index', action='store_false', dest='do_index',
231 help="""Do not index the new data. Only applicable
232 together with --once""")
236 params = ['update.php']
238 params.append('--init-updates')
239 if not args.update_functions:
240 params.append('--no-update-functions')
241 elif args.check_for_updates:
242 params.append('--check-for-updates')
245 params.append('--import-osmosis')
247 params.append('--import-osmosis-all')
248 if not args.do_index:
249 params.append('--no-index')
251 return run_legacy_script(*params, nominatim_env=args)
256 Add additional data from a file or an online source.
258 Data is only imported, not indexed. You need to call `nominatim-update index`
259 to complete the process.
263 def add_args(parser):
264 group_name = parser.add_argument_group('Source')
265 group = group_name.add_mutually_exclusive_group(required=True)
266 group.add_argument('--file', metavar='FILE',
267 help='Import data from an OSM file')
268 group.add_argument('--diff', metavar='FILE',
269 help='Import data from an OSM diff file')
270 group.add_argument('--node', metavar='ID', type=int,
271 help='Import a single node from the API')
272 group.add_argument('--way', metavar='ID', type=int,
273 help='Import a single way from the API')
274 group.add_argument('--relation', metavar='ID', type=int,
275 help='Import a single relation from the API')
276 group.add_argument('--tiger-data', metavar='DIR',
277 help='Add housenumbers from the US TIGER census database.')
278 group = parser.add_argument_group('Extra arguments')
279 group.add_argument('--use-main-api', action='store_true',
280 help='Use OSM API instead of Overpass to download objects')
285 os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
286 return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
288 params = ['update.php']
290 params.extend(('--import-file', args.file))
292 params.extend(('--import-diff', args.diff))
294 params.extend(('--import-node', args.node))
296 params.extend(('--import-way', args.way))
298 params.extend(('--import-relation', args.relation))
299 if args.use_main_api:
300 params.append('--use-main-api')
301 return run_legacy_script(*params, nominatim_env=args)
306 Reindex all new and modified data.
310 def add_args(parser):
311 group = parser.add_argument_group('Filter arguments')
312 group.add_argument('--boundaries-only', action='store_true',
313 help="""Index only administrative boundaries.""")
314 group.add_argument('--no-boundaries', action='store_true',
315 help="""Index everything except administrative boundaries.""")
316 group.add_argument('--minrank', '-r', type=int, metavar='RANK', default=0,
317 help='Minimum/starting rank')
318 group.add_argument('--maxrank', '-R', type=int, metavar='RANK', default=30,
319 help='Maximum/finishing rank')
323 indexer = Indexer(args.config.get_libpq_dsn(),
324 args.threads or _num_system_cpus() or 1)
326 if not args.no_boundaries:
327 indexer.index_boundaries(args.minrank, args.maxrank)
328 if not args.boundaries_only:
329 indexer.index_by_rank(args.minrank, args.maxrank)
331 if not args.no_boundaries and not args.boundaries_only:
332 indexer.update_status_table()
339 Recompute auxiliary data used by the indexing process.
341 These functions must not be run in parallel with other update commands.
345 def add_args(parser):
346 group = parser.add_argument_group('Data arguments')
347 group.add_argument('--postcodes', action='store_true',
348 help='Update postcode centroid table')
349 group.add_argument('--word-counts', action='store_true',
350 help='Compute frequency of full-word search terms')
351 group.add_argument('--address-levels', action='store_true',
352 help='Reimport address level configuration')
353 group.add_argument('--functions', action='store_true',
354 help='Update the PL/pgSQL functions in the database')
355 group.add_argument('--wiki-data', action='store_true',
356 help='Update Wikipedia/data importance numbers.')
357 group.add_argument('--importance', action='store_true',
358 help='Recompute place importances (expensive!)')
359 group.add_argument('--website', action='store_true',
360 help='Refresh the directory that serves the scripts for the web API')
361 group = parser.add_argument_group('Arguments for function refresh')
362 group.add_argument('--no-diff-updates', action='store_false', dest='diffs',
363 help='Do not enable code for propagating updates')
364 group.add_argument('--enable-debug-statements', action='store_true',
365 help='Enable debug warning statements in functions')
370 run_legacy_script('update.php', '--calculate-postcodes',
371 nominatim_env=args, throw_on_fail=True)
373 run_legacy_script('update.php', '--recompute-word-counts',
374 nominatim_env=args, throw_on_fail=True)
375 if args.address_levels:
376 run_legacy_script('update.php', '--update-address-levels',
377 nominatim_env=args, throw_on_fail=True)
379 params = ['setup.php', '--create-functions', '--create-partition-functions']
381 params.append('--enable-diff-updates')
382 if args.enable_debug_statements:
383 params.append('--enable-debug-statements')
384 run_legacy_script(*params, nominatim_env=args, throw_on_fail=True)
386 run_legacy_script('setup.php', '--import-wikipedia-articles',
387 nominatim_env=args, throw_on_fail=True)
388 # Attention: importance MUST come after wiki data import.
390 run_legacy_script('update.php', '--recompute-importance',
391 nominatim_env=args, throw_on_fail=True)
393 run_legacy_script('setup.php', '--setup-website',
394 nominatim_env=args, throw_on_fail=True)
397 class AdminCheckDatabase:
399 Check that the database is complete and operational.
403 def add_args(parser):
408 return run_legacy_script('check_import_finished.php', nominatim_env=args)
413 Warm database caches for search and reverse queries.
417 def add_args(parser):
418 group = parser.add_argument_group('Target arguments')
419 group.add_argument('--search-only', action='store_const', dest='target',
421 help="Only pre-warm tables for search queries")
422 group.add_argument('--reverse-only', action='store_const', dest='target',
424 help="Only pre-warm tables for reverse queries")
428 params = ['warm.php']
429 if args.target == 'reverse':
430 params.append('--reverse-only')
431 if args.target == 'search':
432 params.append('--search-only')
433 return run_legacy_script(*params, nominatim_env=args)
438 Export addresses as CSV file from the database.
442 def add_args(parser):
443 group = parser.add_argument_group('Output arguments')
444 group.add_argument('--output-type', default='street',
445 choices=('continent', 'country', 'state', 'county',
446 'city', 'suburb', 'street', 'path'),
447 help='Type of places to output (default: street)')
448 group.add_argument('--output-format',
449 default='street;suburb;city;county;state;country',
450 help="""Semicolon-separated list of address types
451 (see --output-type). Multiple ranks can be
452 merged into one column by simply using a
453 comma-separated list.""")
454 group.add_argument('--output-all-postcodes', action='store_true',
455 help="""List all postcodes for address instead of
456 just the most likely one""")
457 group.add_argument('--language',
458 help="""Preferred language for output
459 (use local name, if omitted)""")
460 group = parser.add_argument_group('Filter arguments')
461 group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
462 help='Export only objects within country')
463 group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
464 help='Export only children of this OSM node')
465 group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
466 help='Export only children of this OSM way')
467 group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
468 help='Export only children of this OSM relation')
473 params = ['export.php',
474 '--output-type', args.output_type,
475 '--output-format', args.output_format]
476 if args.output_all_postcodes:
477 params.append('--output-all-postcodes')
479 params.extend(('--language', args.language))
480 if args.restrict_to_country:
481 params.extend(('--restrict-to-country', args.restrict_to_country))
482 if args.restrict_to_osm_node:
483 params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
484 if args.restrict_to_osm_way:
485 params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
486 if args.restrict_to_osm_relation:
487 params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
489 return run_legacy_script(*params, nominatim_env=args)
492 ('street', 'housenumber and street'),
493 ('city', 'city, town or village'),
494 ('county', 'county'),
496 ('country', 'country'),
497 ('postalcode', 'postcode')
501 ('addressdetails', 'Include a breakdown of the address into elements.'),
502 ('extratags', """Include additional information if available
503 (e.g. wikipedia link, opening hours)."""),
504 ('namedetails', 'Include a list of alternative names.')
508 ('addressdetails', 'Include a breakdown of the address into elements.'),
509 ('keywords', 'Include a list of name keywords and address keywords.'),
510 ('linkedplaces', 'Include a details of places that are linked with this one.'),
511 ('hierarchy', 'Include details of places lower in the address hierarchy.'),
512 ('group_hierarchy', 'Group the places by type.'),
513 ('polygon_geojson', 'Include geometry of result.')
518 Execute API search query.
522 def add_args(parser):
523 group = parser.add_argument_group('Query arguments')
524 group.add_argument('--query',
525 help='Free-form query string')
526 for name, desc in STRUCTURED_QUERY:
527 group.add_argument('--' + name, help='Structured query: ' + desc)
529 group = parser.add_argument_group('Output arguments')
530 group.add_argument('--format', default='jsonv2',
531 choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
532 help='Format of result')
533 for name, desc in EXTRADATA_PARAMS:
534 group.add_argument('--' + name, action='store_true', help=desc)
536 group.add_argument('--lang', '--accept-language', metavar='LANGS',
537 help='Preferred language order for presenting search results')
538 group.add_argument('--polygon-output',
539 choices=['geojson', 'kml', 'svg', 'text'],
540 help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
541 group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
542 help="""Simplify output geometry.
543 Parameter is difference tolerance in degrees.""")
545 group = parser.add_argument_group('Result limitation')
546 group.add_argument('--countrycodes', metavar='CC,..',
547 help='Limit search results to one or more countries.')
548 group.add_argument('--exclude_place_ids', metavar='ID,..',
549 help='List of search object to be excluded')
550 group.add_argument('--limit', type=int,
551 help='Limit the number of returned results')
552 group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
553 help='Preferred area to find search results')
554 group.add_argument('--bounded', action='store_true',
555 help='Strictly restrict results to viewbox area')
557 group = parser.add_argument_group('Other arguments')
558 group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
559 help='Do not remove duplicates from the result list')
565 params = dict(q=args.query)
567 params = {k : getattr(args, k) for k, _ in STRUCTURED_QUERY if getattr(args, k)}
569 for param, _ in EXTRADATA_PARAMS:
570 if getattr(args, param):
572 for param in ('format', 'countrycodes', 'exclude_place_ids', 'limit', 'viewbox'):
573 if getattr(args, param):
574 params[param] = getattr(args, param)
576 params['accept-language'] = args.lang
577 if args.polygon_output:
578 params['polygon_' + args.polygon_output] = '1'
579 if args.polygon_threshold:
580 params['polygon_threshold'] = args.polygon_threshold
582 params['bounded'] = '1'
584 params['dedupe'] = '0'
586 return run_api_script('search', args.project_dir,
587 phpcgi_bin=args.phpcgi_path, params=params)
591 Execute API reverse query.
595 def add_args(parser):
596 group = parser.add_argument_group('Query arguments')
597 group.add_argument('--lat', type=float, required=True,
598 help='Latitude of coordinate to look up (in WGS84)')
599 group.add_argument('--lon', type=float, required=True,
600 help='Longitude of coordinate to look up (in WGS84)')
601 group.add_argument('--zoom', type=int,
602 help='Level of detail required for the address')
604 group = parser.add_argument_group('Output arguments')
605 group.add_argument('--format', default='jsonv2',
606 choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
607 help='Format of result')
608 for name, desc in EXTRADATA_PARAMS:
609 group.add_argument('--' + name, action='store_true', help=desc)
611 group.add_argument('--lang', '--accept-language', metavar='LANGS',
612 help='Preferred language order for presenting search results')
613 group.add_argument('--polygon-output',
614 choices=['geojson', 'kml', 'svg', 'text'],
615 help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
616 group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
617 help="""Simplify output geometry.
618 Parameter is difference tolerance in degrees.""")
623 params = dict(lat=args.lat, lon=args.lon)
624 if args.zoom is not None:
625 params['zoom'] = args.zoom
627 for param, _ in EXTRADATA_PARAMS:
628 if getattr(args, param):
631 params['format'] = args.format
633 params['accept-language'] = args.lang
634 if args.polygon_output:
635 params['polygon_' + args.polygon_output] = '1'
636 if args.polygon_threshold:
637 params['polygon_threshold'] = args.polygon_threshold
639 return run_api_script('reverse', args.project_dir,
640 phpcgi_bin=args.phpcgi_path, params=params)
645 Execute API reverse query.
649 def add_args(parser):
650 group = parser.add_argument_group('Query arguments')
651 group.add_argument('--id', metavar='OSMID',
652 action='append', required=True, dest='ids',
653 help='OSM id to lookup in format <NRW><id> (may be repeated)')
655 group = parser.add_argument_group('Output arguments')
656 group.add_argument('--format', default='jsonv2',
657 choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
658 help='Format of result')
659 for name, desc in EXTRADATA_PARAMS:
660 group.add_argument('--' + name, action='store_true', help=desc)
662 group.add_argument('--lang', '--accept-language', metavar='LANGS',
663 help='Preferred language order for presenting search results')
664 group.add_argument('--polygon-output',
665 choices=['geojson', 'kml', 'svg', 'text'],
666 help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
667 group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
668 help="""Simplify output geometry.
669 Parameter is difference tolerance in degrees.""")
674 params = dict(osm_ids=','.join(args.ids))
676 for param, _ in EXTRADATA_PARAMS:
677 if getattr(args, param):
680 params['format'] = args.format
682 params['accept-language'] = args.lang
683 if args.polygon_output:
684 params['polygon_' + args.polygon_output] = '1'
685 if args.polygon_threshold:
686 params['polygon_threshold'] = args.polygon_threshold
688 return run_api_script('lookup', args.project_dir,
689 phpcgi_bin=args.phpcgi_path, params=params)
694 Execute API lookup query.
698 def add_args(parser):
699 group = parser.add_argument_group('Query arguments')
700 objs = group.add_mutually_exclusive_group(required=True)
701 objs.add_argument('--node', '-n', type=int,
702 help="Look up the OSM node with the given ID.")
703 objs.add_argument('--way', '-w', type=int,
704 help="Look up the OSM way with the given ID.")
705 objs.add_argument('--relation', '-r', type=int,
706 help="Look up the OSM relation with the given ID.")
707 objs.add_argument('--place_id', '-p', type=int,
708 help='Database internal identifier of the OSM object to look up.')
709 group.add_argument('--class', dest='object_class',
710 help="""Class type to disambiguated multiple entries
711 of the same object.""")
713 group = parser.add_argument_group('Output arguments')
714 for name, desc in DETAILS_SWITCHES:
715 group.add_argument('--' + name, action='store_true', help=desc)
716 group.add_argument('--lang', '--accept-language', metavar='LANGS',
717 help='Preferred language order for presenting search results')
722 params = dict(osmtype='N', osmid=args.node)
724 params = dict(osmtype='W', osmid=args.node)
726 params = dict(osmtype='R', osmid=args.node)
728 params = dict(place_id=args.place_id)
729 if args.object_class:
730 params['class'] = args.object_class
731 for name, _ in DETAILS_SWITCHES:
732 params[name] = '1' if getattr(args, name) else '0'
734 return run_api_script('details', args.project_dir,
735 phpcgi_bin=args.phpcgi_path, params=params)
740 Execute API status query.
744 def add_args(parser):
745 group = parser.add_argument_group('API parameters')
746 group.add_argument('--format', default='text', choices=['text', 'json'],
747 help='Format of result')
751 return run_api_script('status', args.project_dir,
752 phpcgi_bin=args.phpcgi_path,
753 params=dict(format=args.format))
756 def nominatim(**kwargs):
758 Command-line tools for importing, updating, administrating and
759 querying the Nominatim database.
761 parser = CommandlineParser('nominatim', nominatim.__doc__)
763 parser.add_subcommand('import', SetupAll)
764 parser.add_subcommand('freeze', SetupFreeze)
765 parser.add_subcommand('replication', UpdateReplication)
767 parser.add_subcommand('check-database', AdminCheckDatabase)
768 parser.add_subcommand('warm', AdminWarm)
770 parser.add_subcommand('special-phrases', SetupSpecialPhrases)
772 parser.add_subcommand('add-data', UpdateAddData)
773 parser.add_subcommand('index', UpdateIndex)
774 parser.add_subcommand('refresh', UpdateRefresh)
776 parser.add_subcommand('export', QueryExport)
778 if kwargs.get('phpcgi_path'):
779 parser.add_subcommand('search', APISearch)
780 parser.add_subcommand('reverse', APIReverse)
781 parser.add_subcommand('lookup', APILookup)
782 parser.add_subcommand('details', APIDetails)
783 parser.add_subcommand('status', APIStatus)
785 parser.parser.epilog = 'php-cgi not found. Query commands not available.'
787 return parser.run(**kwargs)