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 details API call.
14 import nominatim_api as napi
17 @pytest.mark.parametrize('idobj', (napi.PlaceID(332), napi.OsmID('W', 4),
18 napi.OsmID('W', 4, 'highway')))
19 def test_lookup_in_placex(apiobj, frontend, idobj):
20 import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
21 apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
22 class_='highway', type='residential',
23 name={'name': 'Road'}, address={'city': 'Barrow'},
24 extratags={'surface': 'paved'},
25 parent_place_id=34, linked_place_id=55,
26 admin_level=15, country_code='gb',
28 postcode='34425', wikipedia='en:Faa',
29 rank_search=27, rank_address=26,
32 indexed_date=import_date,
33 geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
35 api = frontend(apiobj, options={'details'})
36 result = api.details(idobj)
37 napi.Locales().localize_results([result])
39 assert result is not None
41 assert result.source_table.name == 'PLACEX'
42 assert result.category == ('highway', 'residential')
43 assert result.centroid == (pytest.approx(23.0), pytest.approx(34.0))
45 assert result.place_id == 332
46 assert result.parent_place_id == 34
47 assert result.linked_place_id == 55
48 assert result.osm_object == ('W', 4)
49 assert result.admin_level == 15
51 assert result.names == {'name': 'Road'}
52 assert result.address == {'city': 'Barrow'}
53 assert result.extratags == {'surface': 'paved'}
55 assert result.housenumber == '4'
56 assert result.postcode == '34425'
57 assert result.wikipedia == 'en:Faa'
59 assert result.rank_search == 27
60 assert result.rank_address == 26
61 assert result.importance == pytest.approx(0.01)
63 assert result.country_code == 'gb'
64 assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
66 assert result.address_rows is None
67 assert result.linked_rows is None
68 assert result.parented_rows is None
69 assert result.name_keywords is None
70 assert result.address_keywords is None
72 assert result.geometry == {'type': 'ST_LineString'}
75 def test_lookup_in_placex_minimal_info(apiobj, frontend):
76 import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
77 apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
78 class_='highway', type='residential',
80 rank_search=27, rank_address=26,
82 indexed_date=import_date,
83 geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
85 api = frontend(apiobj, options={'details'})
86 result = api.details(napi.PlaceID(332))
87 napi.Locales().localize_results([result])
89 assert result is not None
91 assert result.source_table.name == 'PLACEX'
92 assert result.category == ('highway', 'residential')
93 assert result.centroid == (pytest.approx(23.0), pytest.approx(34.0))
95 assert result.place_id == 332
96 assert result.parent_place_id is None
97 assert result.linked_place_id is None
98 assert result.osm_object == ('W', 4)
99 assert result.admin_level == 15
101 assert result.names is None
102 assert result.address is None
103 assert result.extratags is None
105 assert result.housenumber is None
106 assert result.postcode is None
107 assert result.wikipedia is None
109 assert result.rank_search == 27
110 assert result.rank_address == 26
111 assert result.importance is None
113 assert result.country_code is None
114 assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
116 assert result.address_rows is None
117 assert result.linked_rows is None
118 assert result.parented_rows is None
119 assert result.name_keywords is None
120 assert result.address_keywords is None
122 assert result.geometry == {'type': 'ST_LineString'}
125 def test_lookup_in_placex_with_geometry(apiobj, frontend):
126 apiobj.add_placex(place_id=332,
127 geometry='LINESTRING(23 34, 23.1 34)')
129 api = frontend(apiobj, options={'details'})
130 result = api.details(napi.PlaceID(332), geometry_output=napi.GeometryFormat.GEOJSON)
132 assert result.geometry == {'geojson': '{"type":"LineString","coordinates":[[23,34],[23.1,34]]}'}
135 def test_lookup_placex_with_address_details(apiobj, frontend):
136 apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
137 class_='highway', type='residential', name='Street',
139 rank_search=27, rank_address=26)
140 apiobj.add_address_placex(332, fromarea=False, isaddress=False,
142 place_id=1000, osm_type='N', osm_id=3333,
143 class_='place', type='suburb', name='Smallplace',
144 country_code='pl', admin_level=13,
145 rank_search=24, rank_address=23)
146 apiobj.add_address_placex(332, fromarea=True, isaddress=True,
147 place_id=1001, osm_type='N', osm_id=3334,
148 class_='place', type='city', name='Bigplace',
150 rank_search=17, rank_address=16)
152 api = frontend(apiobj, options={'details'})
153 result = api.details(napi.PlaceID(332), address_details=True)
154 napi.Locales().localize_results([result])
156 assert result.address_rows == [
157 napi.AddressLine(place_id=332, osm_object=('W', 4),
158 category=('highway', 'residential'),
159 names={'name': 'Street'}, extratags={},
160 admin_level=15, fromarea=True, isaddress=True,
161 rank_address=26, distance=0.0,
162 local_name='Street'),
163 napi.AddressLine(place_id=1000, osm_object=('N', 3333),
164 category=('place', 'suburb'),
165 names={'name': 'Smallplace'}, extratags={},
166 admin_level=13, fromarea=False, isaddress=True,
167 rank_address=23, distance=0.0034,
168 local_name='Smallplace'),
169 napi.AddressLine(place_id=1001, osm_object=('N', 3334),
170 category=('place', 'city'),
171 names={'name': 'Bigplace'}, extratags={},
172 admin_level=15, fromarea=True, isaddress=True,
173 rank_address=16, distance=0.0,
174 local_name='Bigplace'),
175 napi.AddressLine(place_id=None, osm_object=None,
176 category=('place', 'country_code'),
177 names={'ref': 'pl'}, extratags={},
178 admin_level=None, fromarea=True, isaddress=False,
179 rank_address=4, distance=0.0)
183 def test_lookup_place_with_linked_places_none_existing(apiobj, frontend):
184 apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
185 class_='highway', type='residential', name='Street',
186 country_code='pl', linked_place_id=45,
187 rank_search=27, rank_address=26)
189 api = frontend(apiobj, options={'details'})
190 result = api.details(napi.PlaceID(332), linked_places=True)
192 assert result.linked_rows == []
195 def test_lookup_place_with_linked_places_existing(apiobj, frontend):
196 apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
197 class_='highway', type='residential', name='Street',
198 country_code='pl', linked_place_id=45,
199 rank_search=27, rank_address=26)
200 apiobj.add_placex(place_id=1001, osm_type='W', osm_id=5,
201 class_='highway', type='residential', name='Street',
202 country_code='pl', linked_place_id=332,
203 rank_search=27, rank_address=26)
204 apiobj.add_placex(place_id=1002, osm_type='W', osm_id=6,
205 class_='highway', type='residential', name='Street',
206 country_code='pl', linked_place_id=332,
207 rank_search=27, rank_address=26)
209 api = frontend(apiobj, options={'details'})
210 result = api.details(napi.PlaceID(332), linked_places=True)
212 assert result.linked_rows == [
213 napi.AddressLine(place_id=1001, osm_object=('W', 5),
214 category=('highway', 'residential'),
215 names={'name': 'Street'}, extratags={},
216 admin_level=15, fromarea=False, isaddress=True,
217 rank_address=26, distance=0.0),
218 napi.AddressLine(place_id=1002, osm_object=('W', 6),
219 category=('highway', 'residential'),
220 names={'name': 'Street'}, extratags={},
221 admin_level=15, fromarea=False, isaddress=True,
222 rank_address=26, distance=0.0),
226 def test_lookup_place_with_parented_places_not_existing(apiobj, frontend):
227 apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
228 class_='highway', type='residential', name='Street',
229 country_code='pl', parent_place_id=45,
230 rank_search=27, rank_address=26)
232 api = frontend(apiobj, options={'details'})
233 result = api.details(napi.PlaceID(332), parented_places=True)
235 assert result.parented_rows == []
238 def test_lookup_place_with_parented_places_existing(apiobj, frontend):
239 apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
240 class_='highway', type='residential', name='Street',
241 country_code='pl', parent_place_id=45,
242 rank_search=27, rank_address=26)
243 apiobj.add_placex(place_id=1001, osm_type='N', osm_id=5,
244 class_='place', type='house', housenumber='23',
245 country_code='pl', parent_place_id=332,
246 rank_search=30, rank_address=30)
247 apiobj.add_placex(place_id=1002, osm_type='W', osm_id=6,
248 class_='highway', type='residential', name='Street',
249 country_code='pl', parent_place_id=332,
250 rank_search=27, rank_address=26)
252 api = frontend(apiobj, options={'details'})
253 result = api.details(napi.PlaceID(332), parented_places=True)
255 assert result.parented_rows == [
256 napi.AddressLine(place_id=1001, osm_object=('N', 5),
257 category=('place', 'house'),
258 names={'housenumber': '23'}, extratags={},
259 admin_level=15, fromarea=False, isaddress=True,
260 rank_address=30, distance=0.0),
264 @pytest.mark.parametrize('idobj', (napi.PlaceID(4924), napi.OsmID('W', 9928)))
265 def test_lookup_in_osmline(apiobj, frontend, idobj):
266 import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
267 apiobj.add_osmline(place_id=4924, osm_id=9928,
269 startnumber=1, endnumber=4, step=1,
270 country_code='gb', postcode='34425',
271 address={'city': 'Big'},
272 indexed_date=import_date,
273 geometry='LINESTRING(23 34, 23 35)')
275 api = frontend(apiobj, options={'details'})
276 result = api.details(idobj)
278 assert result is not None
280 assert result.source_table.name == 'OSMLINE'
281 assert result.category == ('place', 'houses')
282 assert result.centroid == (pytest.approx(23.0), pytest.approx(34.5))
284 assert result.place_id == 4924
285 assert result.parent_place_id == 12
286 assert result.linked_place_id is None
287 assert result.osm_object == ('W', 9928)
288 assert result.admin_level == 15
290 assert result.names is None
291 assert result.address == {'city': 'Big'}
292 assert result.extratags == {'startnumber': '1', 'endnumber': '4', 'step': '1'}
294 assert result.housenumber is None
295 assert result.postcode == '34425'
296 assert result.wikipedia is None
298 assert result.rank_search == 30
299 assert result.rank_address == 30
300 assert result.importance is None
302 assert result.country_code == 'gb'
303 assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
305 assert result.address_rows is None
306 assert result.linked_rows is None
307 assert result.parented_rows is None
308 assert result.name_keywords is None
309 assert result.address_keywords is None
311 assert result.geometry == {'type': 'ST_LineString'}
314 def test_lookup_in_osmline_split_interpolation(apiobj, frontend):
315 apiobj.add_osmline(place_id=1000, osm_id=9,
316 startnumber=2, endnumber=4, step=1)
317 apiobj.add_osmline(place_id=1001, osm_id=9,
318 startnumber=6, endnumber=9, step=1)
319 apiobj.add_osmline(place_id=1002, osm_id=9,
320 startnumber=11, endnumber=20, step=1)
322 api = frontend(apiobj, options={'details'})
323 for i in range(1, 6):
324 result = api.details(napi.OsmID('W', 9, str(i)))
325 assert result.place_id == 1000
326 for i in range(7, 11):
327 result = api.details(napi.OsmID('W', 9, str(i)))
328 assert result.place_id == 1001
329 for i in range(12, 22):
330 result = api.details(napi.OsmID('W', 9, str(i)))
331 assert result.place_id == 1002
334 def test_lookup_osmline_with_address_details(apiobj, frontend):
335 apiobj.add_osmline(place_id=9000, osm_id=9,
336 startnumber=2, endnumber=4, step=1,
338 apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
339 class_='highway', type='residential', name='Street',
341 rank_search=27, rank_address=26)
342 apiobj.add_address_placex(332, fromarea=False, isaddress=False,
344 place_id=1000, osm_type='N', osm_id=3333,
345 class_='place', type='suburb', name='Smallplace',
346 country_code='pl', admin_level=13,
347 rank_search=24, rank_address=23)
348 apiobj.add_address_placex(332, fromarea=True, isaddress=True,
349 place_id=1001, osm_type='N', osm_id=3334,
350 class_='place', type='city', name='Bigplace',
352 rank_search=17, rank_address=16)
354 api = frontend(apiobj, options={'details'})
355 result = api.details(napi.PlaceID(9000), address_details=True)
356 napi.Locales().localize_results([result])
358 assert result.address_rows == [
359 napi.AddressLine(place_id=332, osm_object=('W', 4),
360 category=('highway', 'residential'),
361 names={'name': 'Street'}, extratags={},
362 admin_level=15, fromarea=True, isaddress=True,
363 rank_address=26, distance=0.0,
364 local_name='Street'),
365 napi.AddressLine(place_id=1000, osm_object=('N', 3333),
366 category=('place', 'suburb'),
367 names={'name': 'Smallplace'}, extratags={},
368 admin_level=13, fromarea=False, isaddress=True,
369 rank_address=23, distance=0.0034,
370 local_name='Smallplace'),
371 napi.AddressLine(place_id=1001, osm_object=('N', 3334),
372 category=('place', 'city'),
373 names={'name': 'Bigplace'}, extratags={},
374 admin_level=15, fromarea=True, isaddress=True,
375 rank_address=16, distance=0.0,
376 local_name='Bigplace'),
377 napi.AddressLine(place_id=None, osm_object=None,
378 category=('place', 'country_code'),
379 names={'ref': 'pl'}, extratags={},
380 admin_level=None, fromarea=True, isaddress=False,
381 rank_address=4, distance=0.0)
385 def test_lookup_in_tiger(apiobj, frontend):
386 apiobj.add_tiger(place_id=4924,
388 startnumber=1, endnumber=4, step=1,
390 geometry='LINESTRING(23 34, 23 35)')
391 apiobj.add_placex(place_id=12,
392 category=('highway', 'residential'),
393 osm_type='W', osm_id=6601223,
394 geometry='LINESTRING(23 34, 23 35)')
396 api = frontend(apiobj, options={'details'})
397 result = api.details(napi.PlaceID(4924))
399 assert result is not None
401 assert result.source_table.name == 'TIGER'
402 assert result.category == ('place', 'houses')
403 assert result.centroid == (pytest.approx(23.0), pytest.approx(34.5))
405 assert result.place_id == 4924
406 assert result.parent_place_id == 12
407 assert result.linked_place_id is None
408 assert result.osm_object == ('W', 6601223)
409 assert result.admin_level == 15
411 assert result.names is None
412 assert result.address is None
413 assert result.extratags == {'startnumber': '1', 'endnumber': '4', 'step': '1'}
415 assert result.housenumber is None
416 assert result.postcode == '34425'
417 assert result.wikipedia is None
419 assert result.rank_search == 30
420 assert result.rank_address == 30
421 assert result.importance is None
423 assert result.country_code == 'us'
424 assert result.indexed_date is None
426 assert result.address_rows is None
427 assert result.linked_rows is None
428 assert result.parented_rows is None
429 assert result.name_keywords is None
430 assert result.address_keywords is None
432 assert result.geometry == {'type': 'ST_LineString'}
435 def test_lookup_tiger_with_address_details(apiobj, frontend):
436 apiobj.add_tiger(place_id=9000,
437 startnumber=2, endnumber=4, step=1,
439 apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
440 class_='highway', type='residential', name='Street',
442 rank_search=27, rank_address=26)
443 apiobj.add_address_placex(332, fromarea=False, isaddress=False,
445 place_id=1000, osm_type='N', osm_id=3333,
446 class_='place', type='suburb', name='Smallplace',
447 country_code='us', admin_level=13,
448 rank_search=24, rank_address=23)
449 apiobj.add_address_placex(332, fromarea=True, isaddress=True,
450 place_id=1001, osm_type='N', osm_id=3334,
451 class_='place', type='city', name='Bigplace',
453 rank_search=17, rank_address=16)
455 api = frontend(apiobj, options={'details'})
456 result = api.details(napi.PlaceID(9000), address_details=True)
457 napi.Locales().localize_results([result])
459 assert result.address_rows == [
460 napi.AddressLine(place_id=332, osm_object=('W', 4),
461 category=('highway', 'residential'),
462 names={'name': 'Street'}, extratags={},
463 admin_level=15, fromarea=True, isaddress=True,
464 rank_address=26, distance=0.0,
465 local_name='Street'),
466 napi.AddressLine(place_id=1000, osm_object=('N', 3333),
467 category=('place', 'suburb'),
468 names={'name': 'Smallplace'}, extratags={},
469 admin_level=13, fromarea=False, isaddress=True,
470 rank_address=23, distance=0.0034,
471 local_name='Smallplace'),
472 napi.AddressLine(place_id=1001, osm_object=('N', 3334),
473 category=('place', 'city'),
474 names={'name': 'Bigplace'}, extratags={},
475 admin_level=15, fromarea=True, isaddress=True,
476 rank_address=16, distance=0.0,
477 local_name='Bigplace'),
478 napi.AddressLine(place_id=None, osm_object=None,
479 category=('place', 'country_code'),
480 names={'ref': 'us'}, extratags={},
481 admin_level=None, fromarea=True, isaddress=False,
482 rank_address=4, distance=0.0)
486 def test_lookup_in_postcode(apiobj, frontend):
487 import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
488 apiobj.add_postcode(place_id=554,
492 rank_search=20, rank_address=22,
493 indexed_date=import_date,
494 geometry='POINT(-9.45 5.6)')
496 api = frontend(apiobj, options={'details'})
497 result = api.details(napi.PlaceID(554))
499 assert result is not None
501 assert result.source_table.name == 'POSTCODE'
502 assert result.category == ('place', 'postcode')
503 assert result.centroid == (pytest.approx(-9.45), pytest.approx(5.6))
505 assert result.place_id == 554
506 assert result.parent_place_id == 152
507 assert result.linked_place_id is None
508 assert result.osm_object is None
509 assert result.admin_level == 15
511 assert result.names == {'ref': '34 425'}
512 assert result.address is None
513 assert result.extratags is None
515 assert result.housenumber is None
516 assert result.postcode is None
517 assert result.wikipedia is None
519 assert result.rank_search == 20
520 assert result.rank_address == 22
521 assert result.importance is None
523 assert result.country_code == 'gb'
524 assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
526 assert result.address_rows is None
527 assert result.linked_rows is None
528 assert result.parented_rows is None
529 assert result.name_keywords is None
530 assert result.address_keywords is None
532 assert result.geometry == {'type': 'ST_Point'}
535 def test_lookup_postcode_with_address_details(apiobj, frontend):
536 apiobj.add_postcode(place_id=9000,
540 rank_search=25, rank_address=25)
541 apiobj.add_placex(place_id=332, osm_type='N', osm_id=3333,
542 class_='place', type='suburb', name='Smallplace',
543 country_code='gb', admin_level=13,
544 rank_search=24, rank_address=23)
545 apiobj.add_address_placex(332, fromarea=True, isaddress=True,
546 place_id=1001, osm_type='N', osm_id=3334,
547 class_='place', type='city', name='Bigplace',
549 rank_search=17, rank_address=16)
551 api = frontend(apiobj, options={'details'})
552 result = api.details(napi.PlaceID(9000), address_details=True)
553 napi.Locales().localize_results([result])
555 assert result.address_rows == [
556 napi.AddressLine(place_id=9000, osm_object=None,
557 category=('place', 'postcode'),
558 names={'ref': '34 425'}, extratags={},
559 admin_level=15, fromarea=True, isaddress=True,
560 rank_address=25, distance=0.0,
561 local_name='34 425'),
562 napi.AddressLine(place_id=332, osm_object=('N', 3333),
563 category=('place', 'suburb'),
564 names={'name': 'Smallplace'}, extratags={},
565 admin_level=13, fromarea=True, isaddress=True,
566 rank_address=23, distance=0.0,
567 local_name='Smallplace'),
568 napi.AddressLine(place_id=1001, osm_object=('N', 3334),
569 category=('place', 'city'),
570 names={'name': 'Bigplace'}, extratags={},
571 admin_level=15, fromarea=True, isaddress=True,
572 rank_address=16, distance=0.0,
573 local_name='Bigplace'),
574 napi.AddressLine(place_id=None, osm_object=None,
575 category=('place', 'country_code'),
576 names={'ref': 'gb'}, extratags={},
577 admin_level=None, fromarea=True, isaddress=False,
578 rank_address=4, distance=0.0)
582 @pytest.mark.parametrize('objid', [napi.PlaceID(1736),
584 napi.OsmID('N', 55, 'amenity')])
585 def test_lookup_missing_object(apiobj, frontend, objid):
586 apiobj.add_placex(place_id=1, osm_type='N', osm_id=55,
587 class_='place', type='suburb')
589 api = frontend(apiobj, options={'details'})
590 assert api.details(objid) is None
593 @pytest.mark.parametrize('gtype', (napi.GeometryFormat.KML,
594 napi.GeometryFormat.SVG,
595 napi.GeometryFormat.TEXT))
596 def test_lookup_unsupported_geometry(apiobj, frontend, gtype):
597 apiobj.add_placex(place_id=332)
599 api = frontend(apiobj, options={'details'})
600 with pytest.raises(ValueError):
601 api.details(napi.PlaceID(332), geometry_output=gtype)