]> git.openstreetmap.org Git - nominatim.git/blob - src/nominatim_api/v1/format_json.py
Update entrances schema
[nominatim.git] / src / nominatim_api / v1 / format_json.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2024 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Helper functions for output of results in json formats.
9 """
10 from typing import Mapping, Any, Optional, Tuple, Union
11
12 from ..utils.json_writer import JsonWriter
13 from ..results import AddressLines, ReverseResults, SearchResults
14 from . import classtypes as cl
15
16
17 def _write_osm_id(out: JsonWriter, osm_object: Optional[Tuple[str, int]]) -> None:
18     if osm_object is not None:
19         out.keyval_not_none('osm_type', cl.OSM_TYPE_NAME.get(osm_object[0], None))\
20            .keyval('osm_id', osm_object[1])
21
22
23 def _write_typed_address(out: JsonWriter, address: Optional[AddressLines],
24                          country_code: Optional[str]) -> None:
25     parts = {}
26     for line in (address or []):
27         if line.isaddress:
28             if line.local_name:
29                 label = cl.get_label_tag(line.category, line.extratags,
30                                          line.rank_address, country_code)
31                 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']
35
36     for k, v in parts.items():
37         out.keyval(k, v)
38
39     if country_code:
40         out.keyval('country_code', country_code)
41
42
43 def _write_geocodejson_address(out: JsonWriter,
44                                address: Optional[AddressLines],
45                                obj_place_id: Optional[int],
46                                country_code: Optional[str]) -> None:
47     extra = {}
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                 rank_name = GEOCODEJSON_RANKS[line.rank_address]
57                 if rank_name not in extra:
58                     extra[rank_name] = line.local_name
59
60     for k, v in extra.items():
61         out.keyval(k, v)
62
63     if country_code:
64         out.keyval('country_code', country_code)
65
66
67 def format_base_json(results: Union[ReverseResults, SearchResults],
68                      options: Mapping[str, Any], simple: bool,
69                      class_label: str) -> str:
70     """ Return the result list as a simple json string in custom Nominatim format.
71     """
72     out = JsonWriter()
73
74     if simple:
75         if not results:
76             return '{"error":"Unable to geocode"}'
77     else:
78         out.start_array()
79
80     for result in results:
81         out.start_object()\
82              .keyval_not_none('place_id', result.place_id)\
83              .keyval('licence', cl.OSM_ATTRIBUTION)\
84
85         _write_osm_id(out, result.osm_object)
86
87         # lat and lon must be string values
88         out.keyval('lat', f"{result.centroid.lat:0.7f}")\
89            .keyval('lon', f"{result.centroid.lon:0.7f}")\
90            .keyval(class_label, result.category[0])\
91            .keyval('type', result.category[1])\
92            .keyval('place_rank', result.rank_search)\
93            .keyval('importance', result.calculated_importance())\
94            .keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
95                                                    result.rank_address,
96                                                    result.country_code))\
97            .keyval('name', result.locale_name or '')\
98            .keyval('display_name', result.display_name or '')
99
100         if options.get('icon_base_url', None):
101             icon = cl.ICONS.get(result.category)
102             if icon:
103                 out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png")
104
105         if options.get('addressdetails', False):
106             out.key('address').start_object()
107             _write_typed_address(out, result.address_rows, result.country_code)
108             out.end_object().next()
109
110         if options.get('entrances', False) and result.entrances:
111             out.keyval('entrances', result.entrances)
112
113         if options.get('extratags', False):
114             out.keyval('extratags', result.extratags)
115
116         if options.get('namedetails', False):
117             out.keyval('namedetails', result.names)
118
119         # must be string values
120         bbox = cl.bbox_from_result(result)
121         out.key('boundingbox').start_array()\
122            .value(f"{bbox.minlat:0.7f}").next()\
123            .value(f"{bbox.maxlat:0.7f}").next()\
124            .value(f"{bbox.minlon:0.7f}").next()\
125            .value(f"{bbox.maxlon:0.7f}").next()\
126            .end_array().next()
127
128         if result.geometry:
129             for key in ('text', 'kml'):
130                 out.keyval_not_none('geo' + key, result.geometry.get(key))
131             if 'geojson' in result.geometry:
132                 out.key('geojson').raw(result.geometry['geojson']).next()
133             out.keyval_not_none('svg', result.geometry.get('svg'))
134
135         out.end_object()
136
137         if simple:
138             return out()
139
140         out.next()
141
142     out.end_array()
143
144     return out()
145
146
147 def format_base_geojson(results: Union[ReverseResults, SearchResults],
148                         options: Mapping[str, Any],
149                         simple: bool) -> str:
150     """ Return the result list as a geojson string.
151     """
152     if not results and simple:
153         return '{"error":"Unable to geocode"}'
154
155     out = JsonWriter()
156
157     out.start_object()\
158        .keyval('type', 'FeatureCollection')\
159        .keyval('licence', cl.OSM_ATTRIBUTION)\
160        .key('features').start_array()
161
162     for result in results:
163         out.start_object()\
164              .keyval('type', 'Feature')\
165              .key('properties').start_object()
166
167         out.keyval_not_none('place_id', result.place_id)
168
169         _write_osm_id(out, result.osm_object)
170
171         out.keyval('place_rank', result.rank_search)\
172            .keyval('category', result.category[0])\
173            .keyval('type', result.category[1])\
174            .keyval('importance', result.calculated_importance())\
175            .keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
176                                                    result.rank_address,
177                                                    result.country_code))\
178            .keyval('name', result.locale_name or '')\
179            .keyval('display_name', result.display_name or '')
180
181         if options.get('addressdetails', False):
182             out.key('address').start_object()
183             _write_typed_address(out, result.address_rows, result.country_code)
184             out.end_object().next()
185
186         if options.get('entrances', False):
187             out.keyval('entrances', result.entrances)
188
189         if options.get('extratags', False):
190             out.keyval('extratags', result.extratags)
191
192         if options.get('namedetails', False):
193             out.keyval('namedetails', result.names)
194
195         out.end_object().next()  # properties
196
197         out.key('bbox').start_array()
198         for coord in cl.bbox_from_result(result).coords:
199             out.float(coord, 7).next()
200         out.end_array().next()
201
202         out.key('geometry').raw(result.geometry.get('geojson')
203                                 or result.centroid.to_geojson()).next()
204
205         out.end_object().next()
206
207     out.end_array().next().end_object()
208
209     return out()
210
211
212 def format_base_geocodejson(results: Union[ReverseResults, SearchResults],
213                             options: Mapping[str, Any], simple: bool) -> str:
214     """ Return the result list as a geocodejson string.
215     """
216     if not results and simple:
217         return '{"error":"Unable to geocode"}'
218
219     out = JsonWriter()
220
221     out.start_object()\
222        .keyval('type', 'FeatureCollection')\
223        .key('geocoding').start_object()\
224                         .keyval('version', '0.1.0')\
225                         .keyval('attribution', cl.OSM_ATTRIBUTION)\
226                         .keyval('licence', 'ODbL')\
227                         .keyval_not_none('query', options.get('query'))\
228                         .end_object().next()\
229        .key('features').start_array()
230
231     for result in results:
232         out.start_object()\
233              .keyval('type', 'Feature')\
234              .key('properties').start_object()\
235                                .key('geocoding').start_object()
236
237         out.keyval_not_none('place_id', result.place_id)
238
239         _write_osm_id(out, result.osm_object)
240
241         out.keyval('osm_key', result.category[0])\
242            .keyval('osm_value', result.category[1])\
243            .keyval('type', GEOCODEJSON_RANKS[max(3, min(28, result.rank_address))])\
244            .keyval_not_none('accuracy', getattr(result, 'distance', None), transform=int)\
245            .keyval('label', result.display_name or '')\
246            .keyval_not_none('name', result.locale_name or None)\
247
248         if options.get('addressdetails', False):
249             _write_geocodejson_address(out, result.address_rows, result.place_id,
250                                        result.country_code)
251
252             out.key('admin').start_object()
253             if result.address_rows:
254                 for line in result.address_rows:
255                     if line.isaddress and (line.admin_level or 15) < 15 and line.local_name \
256                        and line.category[0] == 'boundary' and line.category[1] == 'administrative':
257                         out.keyval(f"level{line.admin_level}", line.local_name)
258             out.end_object().next()
259
260         if options.get('entrances', False):
261             out.keyval('entrances', result.entrances)
262
263         if options.get('extratags', False):
264             out.keyval('extra', result.extratags)
265
266         out.end_object().next().end_object().next()
267
268         out.key('geometry').raw(result.geometry.get('geojson')
269                                 or result.centroid.to_geojson()).next()
270
271         out.end_object().next()
272
273     out.end_array().next().end_object()
274
275     return out()
276
277
278 GEOCODEJSON_RANKS = {
279     3: 'locality',
280     4: 'country',
281     5: 'state', 6: 'state', 7: 'state', 8: 'state', 9: 'state',
282     10: 'county', 11: 'county', 12: 'county',
283     13: 'city', 14: 'city', 15: 'city', 16: 'city',
284     17: 'district', 18: 'district', 19: 'district', 20: 'district', 21: 'district',
285     22: 'locality', 23: 'locality', 24: 'locality',
286     25: 'street', 26: 'street', 27: 'street', 28: 'house'}