# SPDX-License-Identifier: GPL-3.0-or-later # # This file is part of Nominatim. (https://nominatim.org) # # Copyright (C) 2024 by the Nominatim developer community. # For a full list of authors see the git log. """ Helper functions for output of results in json formats. """ from typing import Mapping, Any, Optional, Tuple, Union from ..utils.json_writer import JsonWriter from ..results import AddressLines, ReverseResults, SearchResults from . import classtypes as cl def _write_osm_id(out: JsonWriter, osm_object: Optional[Tuple[str, int]]) -> None: if osm_object is not None: out.keyval_not_none('osm_type', cl.OSM_TYPE_NAME.get(osm_object[0], None))\ .keyval('osm_id', osm_object[1]) def _write_typed_address(out: JsonWriter, address: Optional[AddressLines], country_code: Optional[str]) -> None: parts = {} for line in (address or []): if line.isaddress: if line.local_name: label = cl.get_label_tag(line.category, line.extratags, line.rank_address, country_code) if label not in parts: parts[label] = line.local_name if line.names and 'ISO3166-2' in line.names and line.admin_level: parts[f"ISO3166-2-lvl{line.admin_level}"] = line.names['ISO3166-2'] for k, v in parts.items(): out.keyval(k, v) if country_code: out.keyval('country_code', country_code) def _write_geocodejson_address(out: JsonWriter, address: Optional[AddressLines], obj_place_id: Optional[int], country_code: Optional[str]) -> None: extra = {} for line in (address or []): if line.isaddress and line.local_name: if line.category[1] in ('postcode', 'postal_code'): out.keyval('postcode', line.local_name) elif line.category[1] == 'house_number': out.keyval('housenumber', line.local_name) elif ((obj_place_id is None or obj_place_id != line.place_id) and line.rank_address >= 4 and line.rank_address < 28): rank_name = GEOCODEJSON_RANKS[line.rank_address] if rank_name not in extra: extra[rank_name] = line.local_name for k, v in extra.items(): out.keyval(k, v) if country_code: out.keyval('country_code', country_code) def format_base_json(results: Union[ReverseResults, SearchResults], options: Mapping[str, Any], simple: bool, class_label: str) -> str: """ Return the result list as a simple json string in custom Nominatim format. """ out = JsonWriter() if simple: if not results: return '{"error":"Unable to geocode"}' else: out.start_array() for result in results: out.start_object()\ .keyval_not_none('place_id', result.place_id)\ .keyval('licence', cl.OSM_ATTRIBUTION)\ _write_osm_id(out, result.osm_object) out.keyval('lat', f"{result.centroid.lat}")\ .keyval('lon', f"{result.centroid.lon}")\ .keyval(class_label, result.category[0])\ .keyval('type', result.category[1])\ .keyval('place_rank', result.rank_search)\ .keyval('importance', result.calculated_importance())\ .keyval('addresstype', cl.get_label_tag(result.category, result.extratags, result.rank_address, result.country_code))\ .keyval('name', result.locale_name or '')\ .keyval('display_name', result.display_name or '') if options.get('icon_base_url', None): icon = cl.ICONS.get(result.category) if icon: out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png") if options.get('addressdetails', False): out.key('address').start_object() _write_typed_address(out, result.address_rows, result.country_code) out.end_object().next() if options.get('extratags', False): out.keyval('extratags', result.extratags) if options.get('namedetails', False): out.keyval('namedetails', result.names) bbox = cl.bbox_from_result(result) out.key('boundingbox').start_array()\ .value(f"{bbox.minlat:0.7f}").next()\ .value(f"{bbox.maxlat:0.7f}").next()\ .value(f"{bbox.minlon:0.7f}").next()\ .value(f"{bbox.maxlon:0.7f}").next()\ .end_array().next() if result.geometry: for key in ('text', 'kml'): out.keyval_not_none('geo' + key, result.geometry.get(key)) if 'geojson' in result.geometry: out.key('geojson').raw(result.geometry['geojson']).next() out.keyval_not_none('svg', result.geometry.get('svg')) out.end_object() if simple: return out() out.next() out.end_array() return out() def format_base_geojson(results: Union[ReverseResults, SearchResults], options: Mapping[str, Any], simple: bool) -> str: """ Return the result list as a geojson string. """ if not results and simple: return '{"error":"Unable to geocode"}' out = JsonWriter() out.start_object()\ .keyval('type', 'FeatureCollection')\ .keyval('licence', cl.OSM_ATTRIBUTION)\ .key('features').start_array() for result in results: out.start_object()\ .keyval('type', 'Feature')\ .key('properties').start_object() out.keyval_not_none('place_id', result.place_id) _write_osm_id(out, result.osm_object) out.keyval('place_rank', result.rank_search)\ .keyval('category', result.category[0])\ .keyval('type', result.category[1])\ .keyval('importance', result.calculated_importance())\ .keyval('addresstype', cl.get_label_tag(result.category, result.extratags, result.rank_address, result.country_code))\ .keyval('name', result.locale_name or '')\ .keyval('display_name', result.display_name or '') if options.get('addressdetails', False): out.key('address').start_object() _write_typed_address(out, result.address_rows, result.country_code) out.end_object().next() if options.get('extratags', False): out.keyval('extratags', result.extratags) if options.get('namedetails', False): out.keyval('namedetails', result.names) out.end_object().next() # properties out.key('bbox').start_array() for coord in cl.bbox_from_result(result).coords: out.float(coord, 7).next() out.end_array().next() out.key('geometry').raw(result.geometry.get('geojson') or result.centroid.to_geojson()).next() out.end_object().next() out.end_array().next().end_object() return out() def format_base_geocodejson(results: Union[ReverseResults, SearchResults], options: Mapping[str, Any], simple: bool) -> str: """ Return the result list as a geocodejson string. """ if not results and simple: return '{"error":"Unable to geocode"}' out = JsonWriter() out.start_object()\ .keyval('type', 'FeatureCollection')\ .key('geocoding').start_object()\ .keyval('version', '0.1.0')\ .keyval('attribution', cl.OSM_ATTRIBUTION)\ .keyval('licence', 'ODbL')\ .keyval_not_none('query', options.get('query'))\ .end_object().next()\ .key('features').start_array() for result in results: out.start_object()\ .keyval('type', 'Feature')\ .key('properties').start_object()\ .key('geocoding').start_object() out.keyval_not_none('place_id', result.place_id) _write_osm_id(out, result.osm_object) out.keyval('osm_key', result.category[0])\ .keyval('osm_value', result.category[1])\ .keyval('type', GEOCODEJSON_RANKS[max(3, min(28, result.rank_address))])\ .keyval_not_none('accuracy', getattr(result, 'distance', None), transform=int)\ .keyval('label', result.display_name or '')\ .keyval_not_none('name', result.locale_name or None)\ if options.get('addressdetails', False): _write_geocodejson_address(out, result.address_rows, result.place_id, result.country_code) out.key('admin').start_object() if result.address_rows: for line in result.address_rows: if line.isaddress and (line.admin_level or 15) < 15 and line.local_name \ and line.category[0] == 'boundary' and line.category[1] == 'administrative': out.keyval(f"level{line.admin_level}", line.local_name) out.end_object().next() if options.get('extratags', False): out.keyval('extra', result.extratags) out.end_object().next().end_object().next() out.key('geometry').raw(result.geometry.get('geojson') or result.centroid.to_geojson()).next() out.end_object().next() out.end_array().next().end_object() return out() GEOCODEJSON_RANKS = { 3: 'locality', 4: 'country', 5: 'state', 6: 'state', 7: 'state', 8: 'state', 9: 'state', 10: 'county', 11: 'county', 12: 'county', 13: 'city', 14: 'city', 15: 'city', 16: 'city', 17: 'district', 18: 'district', 19: 'district', 20: 'district', 21: 'district', 22: 'locality', 23: 'locality', 24: 'locality', 25: 'street', 26: 'street', 27: 'street', 28: 'house'}