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