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)