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 extra[GEOCODEJSON_RANKS[line.rank_address]] = line.local_name
59 for k, v in extra.items():
63 out.keyval('country_code', country_code)
66 def format_base_json(results: Union[napi.ReverseResults, napi.SearchResults],
67 options: Mapping[str, Any], simple: bool,
68 class_label: str) -> str:
69 """ Return the result list as a simple json string in custom Nominatim format.
75 return '{"error":"Unable to geocode"}'
79 for result in results:
81 .keyval_not_none('place_id', result.place_id)\
82 .keyval('licence', cl.OSM_ATTRIBUTION)\
84 _write_osm_id(out, result.osm_object)
86 out.keyval('lat', result.centroid.lat)\
87 .keyval('lon', result.centroid.lon)\
88 .keyval(class_label, result.category[0])\
89 .keyval('type', result.category[1])\
90 .keyval('place_rank', result.rank_search)\
91 .keyval('importance', result.calculated_importance())\
92 .keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
94 result.country_code))\
95 .keyval('name', result.locale_name or '')\
96 .keyval('display_name', result.display_name or '')
99 if options.get('icon_base_url', None):
100 icon = cl.ICONS.get(result.category)
102 out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png")
104 if options.get('addressdetails', False):
105 out.key('address').start_object()
106 _write_typed_address(out, result.address_rows, result.country_code)
107 out.end_object().next()
109 if options.get('extratags', False):
110 out.keyval('extratags', result.extratags)
112 if options.get('namedetails', False):
113 out.keyval('namedetails', result.names)
115 bbox = cl.bbox_from_result(result)
116 out.key('boundingbox').start_array()\
117 .value(f"{bbox.minlat:0.7f}").next()\
118 .value(f"{bbox.maxlat:0.7f}").next()\
119 .value(f"{bbox.minlon:0.7f}").next()\
120 .value(f"{bbox.maxlon:0.7f}").next()\
124 for key in ('text', 'kml'):
125 out.keyval_not_none('geo' + key, result.geometry.get(key))
126 if 'geojson' in result.geometry:
127 out.key('geojson').raw(result.geometry['geojson']).next()
128 out.keyval_not_none('svg', result.geometry.get('svg'))
142 def format_base_geojson(results: Union[napi.ReverseResults, napi.SearchResults],
143 options: Mapping[str, Any],
144 simple: bool) -> str:
145 """ Return the result list as a geojson string.
147 if not results and simple:
148 return '{"error":"Unable to geocode"}'
153 .keyval('type', 'FeatureCollection')\
154 .keyval('licence', cl.OSM_ATTRIBUTION)\
155 .key('features').start_array()
157 for result in results:
159 .keyval('type', 'Feature')\
160 .key('properties').start_object()
162 out.keyval_not_none('place_id', result.place_id)
164 _write_osm_id(out, result.osm_object)
166 out.keyval('place_rank', result.rank_search)\
167 .keyval('category', result.category[0])\
168 .keyval('type', result.category[1])\
169 .keyval('importance', result.calculated_importance())\
170 .keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
172 result.country_code))\
173 .keyval('name', result.locale_name or '')\
174 .keyval('display_name', result.display_name or '')
176 if options.get('addressdetails', False):
177 out.key('address').start_object()
178 _write_typed_address(out, result.address_rows, result.country_code)
179 out.end_object().next()
181 if options.get('extratags', False):
182 out.keyval('extratags', result.extratags)
184 if options.get('namedetails', False):
185 out.keyval('namedetails', result.names)
187 out.end_object().next() # properties
189 out.key('bbox').start_array()
190 for coord in cl.bbox_from_result(result).coords:
191 out.float(coord, 7).next()
192 out.end_array().next()
194 out.key('geometry').raw(result.geometry.get('geojson')
195 or result.centroid.to_geojson()).next()
197 out.end_object().next()
199 out.end_array().next().end_object()
204 def format_base_geocodejson(results: Union[napi.ReverseResults, napi.SearchResults],
205 options: Mapping[str, Any], simple: bool) -> str:
206 """ Return the result list as a geocodejson string.
208 if not results and simple:
209 return '{"error":"Unable to geocode"}'
214 .keyval('type', 'FeatureCollection')\
215 .key('geocoding').start_object()\
216 .keyval('version', '0.1.0')\
217 .keyval('attribution', cl.OSM_ATTRIBUTION)\
218 .keyval('licence', 'ODbL')\
219 .keyval_not_none('query', options.get('query'))\
220 .end_object().next()\
221 .key('features').start_array()
223 for result in results:
225 .keyval('type', 'Feature')\
226 .key('properties').start_object()\
227 .key('geocoding').start_object()
229 out.keyval_not_none('place_id', result.place_id)
231 _write_osm_id(out, result.osm_object)
233 out.keyval('osm_key', result.category[0])\
234 .keyval('osm_value', result.category[1])\
235 .keyval('type', GEOCODEJSON_RANKS[max(3, min(28, result.rank_address))])\
236 .keyval_not_none('accuracy', getattr(result, 'distance', None), transform=int)\
237 .keyval('label', result.display_name or '')\
238 .keyval_not_none('name', result.locale_name or None)\
240 if options.get('addressdetails', False):
241 _write_geocodejson_address(out, result.address_rows, result.place_id,
244 out.key('admin').start_object()
245 if result.address_rows:
246 for line in result.address_rows:
247 if line.isaddress and (line.admin_level or 15) < 15 and line.local_name:
248 out.keyval(f"level{line.admin_level}", line.local_name)
249 out.end_object().next()
251 out.end_object().next().end_object().next()
253 out.key('geometry').raw(result.geometry.get('geojson')
254 or result.centroid.to_geojson()).next()
256 out.end_object().next()
258 out.end_array().next().end_object()
263 GEOCODEJSON_RANKS = {
266 5: 'state', 6: 'state', 7: 'state', 8: 'state', 9: 'state',
267 10: 'county', 11: 'county', 12: 'county',
268 13: 'city', 14: 'city', 15: 'city', 16: 'city',
269 17: 'district', 18: 'district', 19: 'district', 20: 'district', 21: 'district',
270 22: 'locality', 23: 'locality', 24: 'locality',
271 25: 'street', 26: 'street', 27: 'street', 28: 'house'}