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.
71 locales = options.get('locales', napi.Locales())
77 return '{"error":"Unable to geocode"}'
81 for result in results:
82 label_parts = result.address_rows.localize(locales) if result.address_rows else []
85 .keyval_not_none('place_id', result.place_id)\
86 .keyval('licence', cl.OSM_ATTRIBUTION)\
88 _write_osm_id(out, result.osm_object)
90 out.keyval('lat', result.centroid.lat)\
91 .keyval('lon', result.centroid.lon)\
92 .keyval(class_label, result.category[0])\
93 .keyval('type', result.category[1])\
94 .keyval('place_rank', result.rank_search)\
95 .keyval('importance', result.calculated_importance())\
96 .keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
98 result.country_code))\
99 .keyval('name', locales.display_name(result.names))\
100 .keyval('display_name', ', '.join(label_parts))
103 if options.get('icon_base_url', None):
104 icon = cl.ICONS.get(result.category)
106 out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png")
108 if options.get('addressdetails', False):
109 out.key('address').start_object()
110 _write_typed_address(out, result.address_rows, result.country_code)
111 out.end_object().next()
113 if options.get('extratags', False):
114 out.keyval('extratags', result.extratags)
116 if options.get('namedetails', False):
117 out.keyval('namedetails', result.names)
119 bbox = cl.bbox_from_result(result)
120 out.key('boundingbox').start_array()\
121 .value(f"{bbox.minlat:0.7f}").next()\
122 .value(f"{bbox.maxlat:0.7f}").next()\
123 .value(f"{bbox.minlon:0.7f}").next()\
124 .value(f"{bbox.maxlon:0.7f}").next()\
128 for key in ('text', 'kml'):
129 out.keyval_not_none('geo' + key, result.geometry.get(key))
130 if 'geojson' in result.geometry:
131 out.key('geojson').raw(result.geometry['geojson']).next()
132 out.keyval_not_none('svg', result.geometry.get('svg'))
146 def format_base_geojson(results: Union[napi.ReverseResults, napi.SearchResults],
147 options: Mapping[str, Any],
148 simple: bool) -> str:
149 """ Return the result list as a geojson string.
151 if not results and simple:
152 return '{"error":"Unable to geocode"}'
154 locales = options.get('locales', napi.Locales())
159 .keyval('type', 'FeatureCollection')\
160 .keyval('licence', cl.OSM_ATTRIBUTION)\
161 .key('features').start_array()
163 for result in results:
164 if result.address_rows:
165 label_parts = result.address_rows.localize(locales)
170 .keyval('type', 'Feature')\
171 .key('properties').start_object()
173 out.keyval_not_none('place_id', result.place_id)
175 _write_osm_id(out, result.osm_object)
177 out.keyval('place_rank', result.rank_search)\
178 .keyval('category', result.category[0])\
179 .keyval('type', result.category[1])\
180 .keyval('importance', result.calculated_importance())\
181 .keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
183 result.country_code))\
184 .keyval('name', locales.display_name(result.names))\
185 .keyval('display_name', ', '.join(label_parts))
187 if options.get('addressdetails', False):
188 out.key('address').start_object()
189 _write_typed_address(out, result.address_rows, result.country_code)
190 out.end_object().next()
192 if options.get('extratags', False):
193 out.keyval('extratags', result.extratags)
195 if options.get('namedetails', False):
196 out.keyval('namedetails', result.names)
198 out.end_object().next() # properties
200 out.key('bbox').start_array()
201 for coord in cl.bbox_from_result(result).coords:
202 out.float(coord, 7).next()
203 out.end_array().next()
205 out.key('geometry').raw(result.geometry.get('geojson')
206 or result.centroid.to_geojson()).next()
208 out.end_object().next()
210 out.end_array().next().end_object()
215 def format_base_geocodejson(results: Union[napi.ReverseResults, napi.SearchResults],
216 options: Mapping[str, Any], simple: bool) -> str:
217 """ Return the result list as a geocodejson string.
219 if not results and simple:
220 return '{"error":"Unable to geocode"}'
222 locales = options.get('locales', napi.Locales())
227 .keyval('type', 'FeatureCollection')\
228 .key('geocoding').start_object()\
229 .keyval('version', '0.1.0')\
230 .keyval('attribution', cl.OSM_ATTRIBUTION)\
231 .keyval('licence', 'ODbL')\
232 .keyval_not_none('query', options.get('query'))\
233 .end_object().next()\
234 .key('features').start_array()
236 for result in results:
237 if result.address_rows:
238 label_parts = result.address_rows.localize(locales)
243 .keyval('type', 'Feature')\
244 .key('properties').start_object()\
245 .key('geocoding').start_object()
247 out.keyval_not_none('place_id', result.place_id)
249 _write_osm_id(out, result.osm_object)
251 out.keyval('osm_key', result.category[0])\
252 .keyval('osm_value', result.category[1])\
253 .keyval('type', GEOCODEJSON_RANKS[max(3, min(28, result.rank_address))])\
254 .keyval_not_none('accuracy', getattr(result, 'distance', None), transform=int)\
255 .keyval('label', ', '.join(label_parts))\
256 .keyval_not_none('name', result.names, transform=locales.display_name)\
258 if options.get('addressdetails', False):
259 _write_geocodejson_address(out, result.address_rows, result.place_id,
262 out.key('admin').start_object()
263 if result.address_rows:
264 for line in result.address_rows:
265 if line.isaddress and (line.admin_level or 15) < 15 and line.local_name:
266 out.keyval(f"level{line.admin_level}", line.local_name)
267 out.end_object().next()
269 out.end_object().next().end_object().next()
271 out.key('geometry').raw(result.geometry.get('geojson')
272 or result.centroid.to_geojson()).next()
274 out.end_object().next()
276 out.end_array().next().end_object()
281 GEOCODEJSON_RANKS = {
284 5: 'state', 6: 'state', 7: 'state', 8: 'state', 9: 'state',
285 10: 'county', 11: 'county', 12: 'county',
286 13: 'city', 14: 'city', 15: 'city', 16: 'city',
287 17: 'district', 18: 'district', 19: 'district', 20: 'district', 21: 'district',
288 22: 'locality', 23: 'locality', 24: 'locality',
289 25: 'street', 26: 'street', 27: 'street', 28: 'house'}