1 # SPDX-License-Identifier: GPL-3.0-or-later
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2024 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, List
 
  12 from ..utils.json_writer import JsonWriter
 
  13 from ..results import AddressLines, ReverseResults, SearchResults
 
  14 from . import classtypes as cl
 
  15 from ..types import EntranceDetails
 
  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[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[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
 
  61     for k, v in extra.items():
 
  65         out.keyval('country_code', country_code)
 
  68 def write_entrances(out: JsonWriter, entrances: Optional[List[EntranceDetails]]) -> None:
 
  70         out.keyval('entrances', None)
 
  76     for entrance in entrances:
 
  78            .keyval('osm_id', entrance.osm_id)\
 
  79            .keyval('type', entrance.type)\
 
  80            .keyval('lat', f"{entrance.location.lat:0.7f}")\
 
  81            .keyval('lon', f"{entrance.location.lon:0.7f}")
 
  83         if entrance.extratags:
 
  84             out.keyval('extratags', entrance.extratags)
 
  85         out.end_object().next()
 
  87     out.end_array().next()
 
  90 def format_base_json(results: Union[ReverseResults, SearchResults],
 
  91                      options: Mapping[str, Any], simple: bool,
 
  92                      class_label: str) -> str:
 
  93     """ Return the result list as a simple json string in custom Nominatim format.
 
  99             return '{"error":"Unable to geocode"}'
 
 103     for result in results:
 
 105              .keyval_not_none('place_id', result.place_id)\
 
 106              .keyval('licence', cl.OSM_ATTRIBUTION)\
 
 108         _write_osm_id(out, result.osm_object)
 
 110         # lat and lon must be string values
 
 111         out.keyval('lat', f"{result.centroid.lat:0.7f}")\
 
 112            .keyval('lon', f"{result.centroid.lon:0.7f}")\
 
 113            .keyval(class_label, result.category[0])\
 
 114            .keyval('type', result.category[1])\
 
 115            .keyval('place_rank', result.rank_search)\
 
 116            .keyval('importance', result.calculated_importance())\
 
 117            .keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
 
 119                                                    result.country_code))\
 
 120            .keyval('name', result.locale_name or '')\
 
 121            .keyval('display_name', result.display_name or '')
 
 123         if options.get('icon_base_url', None):
 
 124             icon = cl.ICONS.get(result.category)
 
 126                 out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png")
 
 128         if options.get('addressdetails', False):
 
 129             out.key('address').start_object()
 
 130             _write_typed_address(out, result.address_rows, result.country_code)
 
 131             out.end_object().next()
 
 133         if options.get('entrances', False):
 
 134             write_entrances(out, result.entrances)
 
 136         if options.get('extratags', False):
 
 137             out.keyval('extratags', result.extratags)
 
 139         if options.get('namedetails', False):
 
 140             out.keyval('namedetails', result.names)
 
 142         # must be string values
 
 143         bbox = cl.bbox_from_result(result)
 
 144         out.key('boundingbox').start_array()\
 
 145            .value(f"{bbox.minlat:0.7f}").next()\
 
 146            .value(f"{bbox.maxlat:0.7f}").next()\
 
 147            .value(f"{bbox.minlon:0.7f}").next()\
 
 148            .value(f"{bbox.maxlon:0.7f}").next()\
 
 152             for key in ('text', 'kml'):
 
 153                 out.keyval_not_none('geo' + key, result.geometry.get(key))
 
 154             if 'geojson' in result.geometry:
 
 155                 out.key('geojson').raw(result.geometry['geojson']).next()
 
 156             out.keyval_not_none('svg', result.geometry.get('svg'))
 
 170 def format_base_geojson(results: Union[ReverseResults, SearchResults],
 
 171                         options: Mapping[str, Any],
 
 172                         simple: bool) -> str:
 
 173     """ Return the result list as a geojson string.
 
 175     if not results and simple:
 
 176         return '{"error":"Unable to geocode"}'
 
 181        .keyval('type', 'FeatureCollection')\
 
 182        .keyval('licence', cl.OSM_ATTRIBUTION)\
 
 183        .key('features').start_array()
 
 185     for result in results:
 
 187              .keyval('type', 'Feature')\
 
 188              .key('properties').start_object()
 
 190         out.keyval_not_none('place_id', result.place_id)
 
 192         _write_osm_id(out, result.osm_object)
 
 194         out.keyval('place_rank', result.rank_search)\
 
 195            .keyval('category', result.category[0])\
 
 196            .keyval('type', result.category[1])\
 
 197            .keyval('importance', result.calculated_importance())\
 
 198            .keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
 
 200                                                    result.country_code))\
 
 201            .keyval('name', result.locale_name or '')\
 
 202            .keyval('display_name', result.display_name or '')
 
 204         if options.get('addressdetails', False):
 
 205             out.key('address').start_object()
 
 206             _write_typed_address(out, result.address_rows, result.country_code)
 
 207             out.end_object().next()
 
 209         if options.get('entrances', False):
 
 210             write_entrances(out, result.entrances)
 
 212         if options.get('extratags', False):
 
 213             out.keyval('extratags', result.extratags)
 
 215         if options.get('namedetails', False):
 
 216             out.keyval('namedetails', result.names)
 
 218         out.end_object().next()  # properties
 
 220         out.key('bbox').start_array()
 
 221         for coord in cl.bbox_from_result(result).coords:
 
 222             out.float(coord, 7).next()
 
 223         out.end_array().next()
 
 225         out.key('geometry').raw(result.geometry.get('geojson')
 
 226                                 or result.centroid.to_geojson()).next()
 
 228         out.end_object().next()
 
 230     out.end_array().next().end_object()
 
 235 def format_base_geocodejson(results: Union[ReverseResults, SearchResults],
 
 236                             options: Mapping[str, Any], simple: bool) -> str:
 
 237     """ Return the result list as a geocodejson string.
 
 239     if not results and simple:
 
 240         return '{"error":"Unable to geocode"}'
 
 245        .keyval('type', 'FeatureCollection')\
 
 246        .key('geocoding').start_object()\
 
 247                         .keyval('version', '0.1.0')\
 
 248                         .keyval('attribution', cl.OSM_ATTRIBUTION)\
 
 249                         .keyval('licence', 'ODbL')\
 
 250                         .keyval_not_none('query', options.get('query'))\
 
 251                         .end_object().next()\
 
 252        .key('features').start_array()
 
 254     for result in results:
 
 256              .keyval('type', 'Feature')\
 
 257              .key('properties').start_object()\
 
 258                                .key('geocoding').start_object()
 
 260         out.keyval_not_none('place_id', result.place_id)
 
 262         _write_osm_id(out, result.osm_object)
 
 264         out.keyval('osm_key', result.category[0])\
 
 265            .keyval('osm_value', result.category[1])\
 
 266            .keyval('type', GEOCODEJSON_RANKS[max(3, min(28, result.rank_address))])\
 
 267            .keyval_not_none('accuracy', getattr(result, 'distance', None), transform=int)\
 
 268            .keyval('label', result.display_name or '')\
 
 269            .keyval_not_none('name', result.locale_name or None)\
 
 271         if options.get('addressdetails', False):
 
 272             _write_geocodejson_address(out, result.address_rows, result.place_id,
 
 275             out.key('admin').start_object()
 
 276             if result.address_rows:
 
 277                 for line in result.address_rows:
 
 278                     if line.isaddress and (line.admin_level or 15) < 15 and line.local_name \
 
 279                        and line.category[0] == 'boundary' and line.category[1] == 'administrative':
 
 280                         out.keyval(f"level{line.admin_level}", line.local_name)
 
 281             out.end_object().next()
 
 283         if options.get('entrances', False):
 
 284             write_entrances(out, result.entrances)
 
 286         if options.get('extratags', False):
 
 287             out.keyval('extra', result.extratags)
 
 289         out.end_object().next().end_object().next()
 
 291         out.key('geometry').raw(result.geometry.get('geojson')
 
 292                                 or result.centroid.to_geojson()).next()
 
 294         out.end_object().next()
 
 296     out.end_array().next().end_object()
 
 301 GEOCODEJSON_RANKS = {
 
 304     5: 'state', 6: 'state', 7: 'state', 8: 'state', 9: 'state',
 
 305     10: 'county', 11: 'county', 12: 'county',
 
 306     13: 'city', 14: 'city', 15: 'city', 16: 'city',
 
 307     17: 'district', 18: 'district', 19: 'district', 20: 'district', 21: 'district',
 
 308     22: 'locality', 23: 'locality', 24: 'locality',
 
 309     25: 'street', 26: 'street', 27: 'street', 28: 'house'}