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
12 import nominatim.api as napi
13 import nominatim.api.v1.classtypes as cl
14 from nominatim.utils.json_writer import JsonWriter
16 def _write_osm_id(out: JsonWriter, osm_object: Optional[Tuple[str, int]]) -> None:
17 if osm_object is not None:
18 out.keyval_not_none('osm_type', cl.OSM_TYPE_NAME.get(osm_object[0], None))\
19 .keyval('osm_id', osm_object[1])
22 def _write_typed_address(out: JsonWriter, address: Optional[napi.AddressLines],
23 country_code: Optional[str]) -> None:
25 for line in (address or []):
28 label = cl.get_label_tag(line.category, line.extratags,
29 line.rank_address, country_code)
30 if label not in parts:
32 parts[label] = line.local_name
33 if line.names and 'ISO3166-2' in line.names and line.admin_level:
34 parts[f"ISO3166-2-lvl{line.admin_level}"] = line.names['ISO3166-2']
36 for k, v in parts.items():
40 out.keyval('country_code', country_code)
43 def _write_geocodejson_address(out: JsonWriter,
44 address: Optional[napi.AddressLines],
45 obj_place_id: Optional[int],
46 country_code: Optional[str]) -> None:
48 for line in (address or []):
49 if line.isaddress and line.local_name:
50 if line.category[1] in ('postcode', 'postal_code'):
51 out.keyval('postcode', line.local_name)
52 elif line.category[1] == 'house_number':
53 out.keyval('housenumber', line.local_name)
54 elif (obj_place_id is None or obj_place_id != line.place_id) \
55 and line.rank_address >= 4 and line.rank_address < 28:
56 extra[GEOCODEJSON_RANKS[line.rank_address]] = line.local_name
58 for k, v in extra.items():
62 out.keyval('country_code', country_code)
65 def format_base_json(results: napi.ReverseResults, #pylint: disable=too-many-branches
66 options: Mapping[str, Any], simple: bool,
67 class_label: str) -> str:
68 """ Return the result list as a simple json string in custom Nominatim format.
70 locales = options.get('locales', napi.Locales())
76 return '{"error":"Unable to geocode"}'
80 for result in results:
81 label_parts = result.address_rows.localize(locales) if result.address_rows else []
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', locales.display_name(result.names))\
99 .keyval('display_name', ', '.join(label_parts))
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: napi.ReverseResults,
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"}'
153 locales = options.get('locales', napi.Locales())
158 .keyval('type', 'FeatureCollection')\
159 .keyval('licence', cl.OSM_ATTRIBUTION)\
160 .key('features').start_array()
162 for result in results:
163 if result.address_rows:
164 label_parts = result.address_rows.localize(locales)
169 .keyval('type', 'Feature')\
170 .key('properties').start_object()
172 out.keyval_not_none('place_id', result.place_id)
174 _write_osm_id(out, result.osm_object)
176 out.keyval('place_rank', result.rank_search)\
177 .keyval('category', result.category[0])\
178 .keyval('type', result.category[1])\
179 .keyval('importance', result.calculated_importance())\
180 .keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
182 result.country_code))\
183 .keyval('name', locales.display_name(result.names))\
184 .keyval('display_name', ', '.join(label_parts))
186 if options.get('addressdetails', False):
187 out.key('address').start_object()
188 _write_typed_address(out, result.address_rows, result.country_code)
189 out.end_object().next()
191 if options.get('extratags', False):
192 out.keyval('extratags', result.extratags)
194 if options.get('namedetails', False):
195 out.keyval('namedetails', result.names)
197 out.end_object().next() # properties
199 out.key('bbox').start_array()
200 for coord in cl.bbox_from_result(result).coords:
201 out.float(coord, 7).next()
202 out.end_array().next()
204 out.key('geometry').raw(result.geometry.get('geojson')
205 or result.centroid.to_geojson()).next()
207 out.end_object().next()
209 out.end_array().next().end_object()
214 def format_base_geocodejson(results: napi.ReverseResults,
215 options: Mapping[str, Any], simple: bool) -> str:
216 """ Return the result list as a geocodejson string.
218 if not results and simple:
219 return '{"error":"Unable to geocode"}'
221 locales = options.get('locales', napi.Locales())
226 .keyval('type', 'FeatureCollection')\
227 .key('geocoding').start_object()\
228 .keyval('version', '0.1.0')\
229 .keyval('attribution', cl.OSM_ATTRIBUTION)\
230 .keyval('licence', 'ODbL')\
231 .keyval_not_none('query', options.get('query'))\
232 .end_object().next()\
233 .key('features').start_array()
235 for result in results:
236 if result.address_rows:
237 label_parts = result.address_rows.localize(locales)
242 .keyval('type', 'Feature')\
243 .key('properties').start_object()\
244 .key('geocoding').start_object()
246 out.keyval_not_none('place_id', result.place_id)
248 _write_osm_id(out, result.osm_object)
250 out.keyval('osm_key', result.category[0])\
251 .keyval('osm_value', result.category[1])\
252 .keyval('type', GEOCODEJSON_RANKS[max(3, min(28, result.rank_address))])\
253 .keyval_not_none('accuracy', result.distance, transform=int)\
254 .keyval('label', ', '.join(label_parts))\
255 .keyval_not_none('name', result.names, transform=locales.display_name)\
257 if options.get('addressdetails', False):
258 _write_geocodejson_address(out, result.address_rows, result.place_id,
261 out.key('admin').start_object()
262 if result.address_rows:
263 for line in result.address_rows:
264 if line.isaddress and (line.admin_level or 15) < 15 and line.local_name:
265 out.keyval(f"level{line.admin_level}", line.local_name)
266 out.end_object().next()
268 out.end_object().next().end_object().next()
270 out.key('geometry').raw(result.geometry.get('geojson')
271 or result.centroid.to_geojson()).next()
273 out.end_object().next()
275 out.end_array().next().end_object()
280 GEOCODEJSON_RANKS = {
283 5: 'state', 6: 'state', 7: 'state', 8: 'state', 9: 'state',
284 10: 'county', 11: 'county', 12: 'county',
285 13: 'city', 14: 'city', 15: 'city', 16: 'city',
286 17: 'district', 18: 'district', 19: 'district', 20: 'district', 21: 'district',
287 22: 'locality', 23: 'locality', 24: 'locality',
288 25: 'street', 26: 'street', 27: 'street', 28: 'house'}