1 # SPDX-License-Identifier: GPL-3.0-or-later
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2023 by the Nominatim developer community.
 
   6 # For a full list of authors see the git log.
 
   8 Helper functions for output of results in json formats.
 
  10 from typing import Mapping, Any, Optional, Tuple, Union
 
  12 import nominatim.api as napi
 
  13 import nominatim.api.v1.classtypes as cl
 
  14 from nominatim.utils.json_writer import JsonWriter
 
  16 #pylint: disable=too-many-branches
 
  18 def _write_osm_id(out: JsonWriter, osm_object: Optional[Tuple[str, int]]) -> None:
 
  19     if osm_object is not None:
 
  20         out.keyval_not_none('osm_type', cl.OSM_TYPE_NAME.get(osm_object[0], None))\
 
  21            .keyval('osm_id', osm_object[1])
 
  24 def _write_typed_address(out: JsonWriter, address: Optional[napi.AddressLines],
 
  25                                country_code: Optional[str]) -> None:
 
  27     for line in (address or []):
 
  30                 label = cl.get_label_tag(line.category, line.extratags,
 
  31                                          line.rank_address, country_code)
 
  32                 if label not in parts:
 
  33                     parts[label] = line.local_name
 
  34             if line.names and 'ISO3166-2' in line.names and line.admin_level:
 
  35                 parts[f"ISO3166-2-lvl{line.admin_level}"] = line.names['ISO3166-2']
 
  37     for k, v in parts.items():
 
  41         out.keyval('country_code', country_code)
 
  44 def _write_geocodejson_address(out: JsonWriter,
 
  45                                address: Optional[napi.AddressLines],
 
  46                                obj_place_id: Optional[int],
 
  47                                country_code: Optional[str]) -> None:
 
  49     for line in (address or []):
 
  50         if line.isaddress and line.local_name:
 
  51             if line.category[1] in ('postcode', 'postal_code'):
 
  52                 out.keyval('postcode', line.local_name)
 
  53             elif line.category[1] == 'house_number':
 
  54                 out.keyval('housenumber', line.local_name)
 
  55             elif (obj_place_id is None or obj_place_id != line.place_id) \
 
  56                  and line.rank_address >= 4 and line.rank_address < 28:
 
  57                 rank_name = GEOCODEJSON_RANKS[line.rank_address]
 
  58                 if rank_name not in extra:
 
  59                     extra[rank_name] = line.local_name
 
  62     for k, v in extra.items():
 
  66         out.keyval('country_code', country_code)
 
  69 def format_base_json(results: Union[napi.ReverseResults, napi.SearchResults],
 
  70                      options: Mapping[str, Any], simple: bool,
 
  71                      class_label: str) -> str:
 
  72     """ Return the result list as a simple json string in custom Nominatim format.
 
  78             return '{"error":"Unable to geocode"}'
 
  82     for result in results:
 
  84              .keyval_not_none('place_id', result.place_id)\
 
  85              .keyval('licence', cl.OSM_ATTRIBUTION)\
 
  87         _write_osm_id(out, result.osm_object)
 
  89         out.keyval('lat', result.centroid.lat)\
 
  90              .keyval('lon', result.centroid.lon)\
 
  91              .keyval(class_label, result.category[0])\
 
  92              .keyval('type', result.category[1])\
 
  93              .keyval('place_rank', result.rank_search)\
 
  94              .keyval('importance', result.calculated_importance())\
 
  95              .keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
 
  97                                                      result.country_code))\
 
  98              .keyval('name', result.locale_name or '')\
 
  99              .keyval('display_name', result.display_name or '')
 
 102         if options.get('icon_base_url', None):
 
 103             icon = cl.ICONS.get(result.category)
 
 105                 out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png")
 
 107         if options.get('addressdetails', False):
 
 108             out.key('address').start_object()
 
 109             _write_typed_address(out, result.address_rows, result.country_code)
 
 110             out.end_object().next()
 
 112         if options.get('extratags', False):
 
 113             out.keyval('extratags', result.extratags)
 
 115         if options.get('namedetails', False):
 
 116             out.keyval('namedetails', result.names)
 
 118         bbox = cl.bbox_from_result(result)
 
 119         out.key('boundingbox').start_array()\
 
 120              .value(f"{bbox.minlat:0.7f}").next()\
 
 121              .value(f"{bbox.maxlat:0.7f}").next()\
 
 122              .value(f"{bbox.minlon:0.7f}").next()\
 
 123              .value(f"{bbox.maxlon:0.7f}").next()\
 
 127             for key in ('text', 'kml'):
 
 128                 out.keyval_not_none('geo' + key, result.geometry.get(key))
 
 129             if 'geojson' in result.geometry:
 
 130                 out.key('geojson').raw(result.geometry['geojson']).next()
 
 131             out.keyval_not_none('svg', result.geometry.get('svg'))
 
 145 def format_base_geojson(results: Union[napi.ReverseResults, napi.SearchResults],
 
 146                         options: Mapping[str, Any],
 
 147                         simple: bool) -> str:
 
 148     """ Return the result list as a geojson string.
 
 150     if not results and simple:
 
 151         return '{"error":"Unable to geocode"}'
 
 156          .keyval('type', 'FeatureCollection')\
 
 157          .keyval('licence', cl.OSM_ATTRIBUTION)\
 
 158          .key('features').start_array()
 
 160     for result in results:
 
 162              .keyval('type', 'Feature')\
 
 163              .key('properties').start_object()
 
 165         out.keyval_not_none('place_id', result.place_id)
 
 167         _write_osm_id(out, result.osm_object)
 
 169         out.keyval('place_rank', result.rank_search)\
 
 170            .keyval('category', result.category[0])\
 
 171            .keyval('type', result.category[1])\
 
 172            .keyval('importance', result.calculated_importance())\
 
 173            .keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
 
 175                                                    result.country_code))\
 
 176            .keyval('name', result.locale_name or '')\
 
 177            .keyval('display_name', result.display_name or '')
 
 179         if options.get('addressdetails', False):
 
 180             out.key('address').start_object()
 
 181             _write_typed_address(out, result.address_rows, result.country_code)
 
 182             out.end_object().next()
 
 184         if options.get('extratags', False):
 
 185             out.keyval('extratags', result.extratags)
 
 187         if options.get('namedetails', False):
 
 188             out.keyval('namedetails', result.names)
 
 190         out.end_object().next() # properties
 
 192         out.key('bbox').start_array()
 
 193         for coord in cl.bbox_from_result(result).coords:
 
 194             out.float(coord, 7).next()
 
 195         out.end_array().next()
 
 197         out.key('geometry').raw(result.geometry.get('geojson')
 
 198                                 or result.centroid.to_geojson()).next()
 
 200         out.end_object().next()
 
 202     out.end_array().next().end_object()
 
 207 def format_base_geocodejson(results: Union[napi.ReverseResults, napi.SearchResults],
 
 208                             options: Mapping[str, Any], simple: bool) -> str:
 
 209     """ Return the result list as a geocodejson string.
 
 211     if not results and simple:
 
 212         return '{"error":"Unable to geocode"}'
 
 217          .keyval('type', 'FeatureCollection')\
 
 218          .key('geocoding').start_object()\
 
 219            .keyval('version', '0.1.0')\
 
 220            .keyval('attribution', cl.OSM_ATTRIBUTION)\
 
 221            .keyval('licence', 'ODbL')\
 
 222            .keyval_not_none('query', options.get('query'))\
 
 223            .end_object().next()\
 
 224          .key('features').start_array()
 
 226     for result in results:
 
 228              .keyval('type', 'Feature')\
 
 229              .key('properties').start_object()\
 
 230                .key('geocoding').start_object()
 
 232         out.keyval_not_none('place_id', result.place_id)
 
 234         _write_osm_id(out, result.osm_object)
 
 236         out.keyval('osm_key', result.category[0])\
 
 237            .keyval('osm_value', result.category[1])\
 
 238            .keyval('type', GEOCODEJSON_RANKS[max(3, min(28, result.rank_address))])\
 
 239            .keyval_not_none('accuracy', getattr(result, 'distance', None), transform=int)\
 
 240            .keyval('label', result.display_name or '')\
 
 241            .keyval_not_none('name', result.locale_name or None)\
 
 243         if options.get('addressdetails', False):
 
 244             _write_geocodejson_address(out, result.address_rows, result.place_id,
 
 247             out.key('admin').start_object()
 
 248             if result.address_rows:
 
 249                 for line in result.address_rows:
 
 250                     if line.isaddress and (line.admin_level or 15) < 15 and line.local_name:
 
 251                         out.keyval(f"level{line.admin_level}", line.local_name)
 
 252             out.end_object().next()
 
 254         out.end_object().next().end_object().next()
 
 256         out.key('geometry').raw(result.geometry.get('geojson')
 
 257                                 or result.centroid.to_geojson()).next()
 
 259         out.end_object().next()
 
 261     out.end_array().next().end_object()
 
 266 GEOCODEJSON_RANKS = {
 
 269     5: 'state', 6: 'state', 7: 'state', 8: 'state', 9: 'state',
 
 270     10: 'county', 11: 'county', 12: 'county',
 
 271     13: 'city', 14: 'city', 15: 'city', 16: 'city',
 
 272     17: 'district', 18: 'district', 19: 'district', 20: 'district', 21: 'district',
 
 273     22: 'locality', 23: 'locality', 24: 'locality',
 
 274     25: 'street', 26: 'street', 27: 'street', 28: 'house'}