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', f"{result.centroid.lat}")\
90 .keyval('lon', f"{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 and line.category[0] == 'boundary' and line.category[1] == 'administrative':
252 out.keyval(f"level{line.admin_level}", line.local_name)
253 out.end_object().next()
255 out.end_object().next().end_object().next()
257 out.key('geometry').raw(result.geometry.get('geojson')
258 or result.centroid.to_geojson()).next()
260 out.end_object().next()
262 out.end_array().next().end_object()
267 GEOCODEJSON_RANKS = {
270 5: 'state', 6: 'state', 7: 'state', 8: 'state', 9: 'state',
271 10: 'county', 11: 'county', 12: 'county',
272 13: 'city', 14: 'city', 15: 'city', 16: 'city',
273 17: 'district', 18: 'district', 19: 'district', 20: 'district', 21: 'district',
274 22: 'locality', 23: 'locality', 24: 'locality',
275 25: 'street', 26: 'street', 27: 'street', 28: 'house'}