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