From 0793f91e8122a61e2d047a9600bf1c19c53e2139 Mon Sep 17 00:00:00 2001 From: Itz-Agasta Date: Thu, 21 May 2026 02:25:15 +0530 Subject: [PATCH] Add stable postcode reference type support - add `PostcodeRef` and parsing helpers - allow postcode refs in excluded IDs - export the new public API type --- src/nominatim_api/__init__.py | 1 + src/nominatim_api/types.py | 39 +++++++++++++++++++++++++++++-- test/python/api/test_api_types.py | 12 ++++++++-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/nominatim_api/__init__.py b/src/nominatim_api/__init__.py index bdef4258..bb50170e 100644 --- a/src/nominatim_api/__init__.py +++ b/src/nominatim_api/__init__.py @@ -20,6 +20,7 @@ from .connection import (SearchConnection as SearchConnection) from .status import (StatusResult as StatusResult) from .types import (PlaceID as PlaceID, OsmID as OsmID, + PostcodeRef as PostcodeRef, PlaceRef as PlaceRef, Point as Point, Bbox as Bbox, diff --git a/src/nominatim_api/types.py b/src/nominatim_api/types.py index 9a15325e..547fb0ee 100644 --- a/src/nominatim_api/types.py +++ b/src/nominatim_api/types.py @@ -81,7 +81,40 @@ class OsmID: return None -PlaceRef = Union[PlaceID, OsmID] +@dataclasses.dataclass +class PostcodeRef: + """ Reference an artificial postcode by country code and postcode. + """ + country_code: str + postcode: str + + def __post_init__(self) -> None: + if len(self.country_code) != 2 or not self.country_code.isalpha(): + raise ValueError('Country code must be two letters.') + if not self.postcode: + raise ValueError('Postcode must not be empty.') + self.country_code = self.country_code.lower() + + def __str__(self) -> str: + return f"P{self.country_code}:{self.postcode}" + + +PlaceRef = Union[PlaceID, OsmID, PostcodeRef] + + +def parse_place_ref(ref: Any) -> PlaceRef: + """ Parse a stable place reference string. + """ + if isinstance(ref, (PlaceID, OsmID, PostcodeRef)): + return ref + + if not isinstance(ref, str): + raise UsageError("Parameter 'place_ref' must be a string.") + + if len(ref) > 4 and ref[0] == 'P' and ref[1:3].isalpha() and ref[3] == ':' and ref[4:]: + return PostcodeRef(ref[1:3], ref[4:]) + + raise UsageError(f"Invalid place_ref: {ref}") class Point(NamedTuple): @@ -427,7 +460,7 @@ def format_excluded(ids: Any) -> List[PlaceRef]: for i in plist: if not i: continue - if isinstance(i, (PlaceID, OsmID)): + if isinstance(i, (PlaceID, OsmID, PostcodeRef)): result.append(i) elif isinstance(i, int): if i > 0: @@ -439,6 +472,8 @@ def format_excluded(ids: Any) -> List[PlaceRef]: elif len(i) > 1 and i[0].upper() in ('N', 'W', 'R') and i[1:].isdigit(): if int(i[1:]) > 0: result.append(OsmID(i[0].upper(), int(i[1:]))) + elif len(i) > 4 and i[0] == 'P' and i[1:3].isalpha() and i[3] == ':' and i[4:]: + result.append(PostcodeRef(i[1:3], i[4:])) else: raise UsageError(f"Invalid exclude ID: {i}") else: diff --git a/test/python/api/test_api_types.py b/test/python/api/test_api_types.py index a67a631c..4f49c860 100644 --- a/test/python/api/test_api_types.py +++ b/test/python/api/test_api_types.py @@ -48,9 +48,13 @@ class TestFormatExcluded: ('N100', [typ.OsmID('N', 100)]), ('W101', [typ.OsmID('W', 101)]), ('R102', [typ.OsmID('R', 102)]), + ('Pus:94110', [typ.PostcodeRef('us', '94110')]), + ('Pgb:EH4 7EA', [typ.PostcodeRef('gb', 'EH4 7EA')]), ('N100,W101,R102', [typ.OsmID('N', 100), typ.OsmID('W', 101), typ.OsmID('R', 102)]), ('n100', [typ.OsmID('N', 100)]), - ('123,N456,W789', [typ.PlaceID(123), typ.OsmID('N', 456), typ.OsmID('W', 789)]), + ('123,N456,W789,Pus:94110', [typ.PlaceID(123), typ.OsmID('N', 456), + typ.OsmID('W', 789), + typ.PostcodeRef('us', '94110')]), (' 123 , N456 ', [typ.PlaceID(123), typ.OsmID('N', 456)]), ('123,,456', [typ.PlaceID(123), typ.PlaceID(456)]), ('0', []), @@ -66,7 +70,11 @@ class TestFormatExcluded: ('-540', '-540'), ('N-100', 'N-100'), ('123,abc,456', 'abc'), - ('N:100', 'N:100') + ('N:100', 'N:100'), + ('Pus', 'Pus'), + ('P:94110', 'P:94110'), + ('Pusa:94110', 'Pusa:94110'), + ('Pus:', 'Pus:') ]) def test_invalid_exclude_ids(self, inp, bad_id): with pytest.raises(UsageError, match=f"Invalid exclude ID: {bad_id}"): -- 2.47.3