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 reverse.localize(napi.Locales())
107 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
108 {'addressdetails': True})
111 root = ET.fromstring(raw)
112 assert root.find('addressparts').find('county').text == 'Hello'
114 result = json.loads(raw)
115 assert isinstance(result, dict)
117 if fmt == 'geocodejson':
118 props = result['features'][0]['properties']['geocoding']
119 assert 'admin' in props
120 assert props['county'] == 'Hello'
123 props = result['features'][0]['properties']
126 assert 'address' in props
129 def test_format_reverse_geocodejson_special_parts():
130 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
132 napi.Point(1.0, 2.0),
135 address_rows=napi.AddressLines([
136 napi.AddressLine(place_id=None,
138 category=('place', 'house_number'),
146 napi.AddressLine(place_id=None,
148 category=('place', 'postcode'),
149 names={'ref': '99446'},
156 napi.AddressLine(place_id=33,
158 category=('place', 'county'),
159 names={'name': 'Hello'},
168 reverse.localize(napi.Locales())
170 raw = v1_format.format_result(napi.ReverseResults([reverse]), 'geocodejson',
171 {'addressdetails': True})
173 props = json.loads(raw)['features'][0]['properties']['geocoding']
174 assert props['housenumber'] == '1'
175 assert props['postcode'] == '99446'
176 assert 'county' not in props
179 @pytest.mark.parametrize('fmt', FORMATS)
180 def test_format_reverse_with_address_none(fmt):
181 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
183 napi.Point(1.0, 2.0),
184 address_rows=napi.AddressLines())
186 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
187 {'addressdetails': True})
190 root = ET.fromstring(raw)
191 assert root.find('addressparts') is None
193 result = json.loads(raw)
194 assert isinstance(result, dict)
196 if fmt == 'geocodejson':
197 props = result['features'][0]['properties']['geocoding']
199 assert 'admin' in props
202 props = result['features'][0]['properties']
205 assert 'address' in props
208 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
209 def test_format_reverse_with_extratags(fmt):
210 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
212 napi.Point(1.0, 2.0),
213 extratags={'one': 'A', 'two': 'B'})
215 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
219 root = ET.fromstring(raw)
220 assert root.find('extratags').find('tag').attrib['key'] == 'one'
222 result = json.loads(raw)
224 extra = result['features'][0]['properties']['extratags']
226 extra = result['extratags']
228 assert extra == {'one': 'A', 'two': 'B'}
231 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
232 def test_format_reverse_with_extratags_none(fmt):
233 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
235 napi.Point(1.0, 2.0))
237 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
241 root = ET.fromstring(raw)
242 assert root.find('extratags') is not None
244 result = json.loads(raw)
246 extra = result['features'][0]['properties']['extratags']
248 extra = result['extratags']
253 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
254 def test_format_reverse_with_namedetails_with_name(fmt):
255 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
257 napi.Point(1.0, 2.0),
258 names={'name': 'A', 'ref': '1'})
260 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
261 {'namedetails': True})
264 root = ET.fromstring(raw)
265 assert root.find('namedetails').find('name').text == 'A'
267 result = json.loads(raw)
269 extra = result['features'][0]['properties']['namedetails']
271 extra = result['namedetails']
273 assert extra == {'name': 'A', 'ref': '1'}
276 @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
277 def test_format_reverse_with_namedetails_without_name(fmt):
278 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
280 napi.Point(1.0, 2.0))
282 raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
283 {'namedetails': True})
286 root = ET.fromstring(raw)
287 assert root.find('namedetails') is not None
289 result = json.loads(raw)
291 extra = result['features'][0]['properties']['namedetails']
293 extra = result['namedetails']
298 @pytest.mark.parametrize('fmt', ['json', 'jsonv2'])
299 def test_search_details_with_icon_available(fmt):
300 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
301 ('amenity', 'restaurant'),
302 napi.Point(1.0, 2.0))
304 result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
305 {'icon_base_url': 'foo'})
307 js = json.loads(result)
309 assert js['icon'] == 'foo/food_restaurant.p.20.png'
312 @pytest.mark.parametrize('fmt', ['json', 'jsonv2'])
313 def test_search_details_with_icon_not_available(fmt):
314 reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
316 napi.Point(1.0, 2.0))
318 result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
319 {'icon_base_url': 'foo'})
321 assert 'icon' not in json.loads(result)