1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2025 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Tests for formatting reverse results for the V1 API.
10 These test only ensure that the Python code is correct.
11 For functional tests see BDD test suite.
14 import xml.etree.ElementTree as ET
18 from nominatim_api.v1.format import dispatch as v1_format
19 import nominatim_api as napi
21 FORMATS = ['json', 'jsonv2', 'geojson', 'geocodejson', 'xml']
24 @pytest.mark.parametrize('fmt', FORMATS)
25 def test_format_reverse_minimal(fmt):
26 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
27 ('amenity', 'post_box'),
28 napi.Point(0.3, -8.9))
30 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
33 root = ET.fromstring(raw)
34 assert root.tag == 'reversegeocode'
36 result = json.loads(raw)
37 assert isinstance(result, dict)
40 @pytest.mark.parametrize('fmt', FORMATS)
41 def test_format_reverse_no_result(fmt):
42 raw = v1_format.format_result(napi.ReverseResults(), fmt, {})
45 root = ET.fromstring(raw)
46 assert root.find('error').text == 'Unable to geocode'
48 assert json.loads(raw) == {'error': 'Unable to geocode'}
51 @pytest.mark.parametrize('fmt', FORMATS)
52 def test_format_reverse_with_osm_id(fmt):
53 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
54 ('amenity', 'post_box'),
55 napi.Point(0.3, -8.9),
59 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
62 root = ET.fromstring(raw).find('result')
63 assert root.attrib['osm_type'] == 'node'
64 assert root.attrib['osm_id'] == '23'
66 result = json.loads(raw)
67 if fmt == 'geocodejson':
68 props = result['features'][0]['properties']['geocoding']
69 elif fmt == 'geojson':
70 props = result['features'][0]['properties']
73 assert props['osm_type'] == 'node'
74 assert props['osm_id'] == 23
77 @pytest.mark.parametrize('fmt', FORMATS)
78 def test_format_reverse_with_address(fmt):
79 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
83 address_rows=napi.AddressLines([
84 napi.AddressLine(place_id=None,
86 category=('place', 'county'),
87 names={'name': 'Hello'},
94 napi.AddressLine(place_id=None,
96 category=('place', 'county'),
97 names={'name': 'ByeBye'},
105 napi.Locales().localize_results([reverse])
107 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
108 {'addressdetails': True})
110 root = ET.fromstring(raw)
111 assert root.find('addressparts').find('county').text == 'Hello'
113 result = json.loads(raw)
114 assert isinstance(result, dict)
116 if fmt == 'geocodejson':
117 props = result['features'][0]['properties']['geocoding']
118 assert 'admin' in props
119 assert props['county'] == 'Hello'
122 props = result['features'][0]['properties']
125 assert 'address' in props
128 def test_format_reverse_geocodejson_special_parts():
129 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
131 napi.Point(1.0, 2.0),
134 address_rows=napi.AddressLines([
135 napi.AddressLine(place_id=None,
137 category=('place', 'house_number'),
145 napi.AddressLine(place_id=None,
147 category=('place', 'postcode'),
148 names={'ref': '99446'},
155 napi.AddressLine(place_id=33,
157 category=('place', 'county'),
158 names={'name': 'Hello'},
167 napi.Locales().localize_results([reverse])
169 raw = v1_format.format_result(napi.ReverseResults([reverse]), 'geocodejson',
170 {'addressdetails': True})
172 props = json.loads(raw)['features'][0]['properties']['geocoding']
173 assert props['housenumber'] == '1'
174 assert props['postcode'] == '99446'
175 assert 'county' not in props
178 @pytest.mark.parametrize('fmt', FORMATS)
179 def test_format_reverse_with_address_none(fmt):
180 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
182 napi.Point(1.0, 2.0),
183 address_rows=napi.AddressLines())
185 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
186 {'addressdetails': True})
189 root = ET.fromstring(raw)
190 assert root.find('addressparts') is None
192 result = json.loads(raw)
193 assert isinstance(result, dict)
195 if fmt == 'geocodejson':
196 props = result['features'][0]['properties']['geocoding']
198 assert 'admin' in props
201 props = result['features'][0]['properties']
204 assert 'address' in props
207 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
208 def test_format_reverse_with_extratags(fmt):
209 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
211 napi.Point(1.0, 2.0),
212 extratags={'one': 'A', 'two': 'B'})
214 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
218 root = ET.fromstring(raw)
219 assert root.find('extratags').find('tag').attrib['key'] == 'one'
221 result = json.loads(raw)
223 extra = result['features'][0]['properties']['extratags']
225 extra = result['extratags']
227 assert extra == {'one': 'A', 'two': 'B'}
230 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
231 def test_format_reverse_with_extratags_none(fmt):
232 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
234 napi.Point(1.0, 2.0))
236 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
240 root = ET.fromstring(raw)
241 assert root.find('extratags') is not None
243 result = json.loads(raw)
245 extra = result['features'][0]['properties']['extratags']
247 extra = result['extratags']
252 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
253 def test_format_reverse_with_namedetails_with_name(fmt):
254 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
256 napi.Point(1.0, 2.0),
257 names={'name': 'A', 'ref': '1'})
259 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
260 {'namedetails': True})
263 root = ET.fromstring(raw)
264 assert root.find('namedetails').find('name').text == 'A'
266 result = json.loads(raw)
268 extra = result['features'][0]['properties']['namedetails']
270 extra = result['namedetails']
272 assert extra == {'name': 'A', 'ref': '1'}
275 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
276 def test_format_reverse_with_namedetails_without_name(fmt):
277 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
279 napi.Point(1.0, 2.0))
281 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
282 {'namedetails': True})
285 root = ET.fromstring(raw)
286 assert root.find('namedetails') is not None
288 result = json.loads(raw)
290 extra = result['features'][0]['properties']['namedetails']
292 extra = result['namedetails']
297 @pytest.mark.parametrize('fmt', ['json', 'jsonv2'])
298 def test_search_details_with_icon_available(fmt):
299 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
300 ('amenity', 'restaurant'),
301 napi.Point(1.0, 2.0))
303 result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
304 {'icon_base_url': 'foo'})
306 js = json.loads(result)
308 assert js['icon'] == 'foo/food_restaurant.p.20.png'
311 @pytest.mark.parametrize('fmt', ['json', 'jsonv2'])
312 def test_search_details_with_icon_not_available(fmt):
313 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
315 napi.Point(1.0, 2.0))
317 result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
318 {'icon_base_url': 'foo'})
320 assert 'icon' not in json.loads(result)