From 409ac636774eb068e1c7feceb2a84f7dabd1f316 Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Thu, 6 Aug 2026 21:19:29 +0200 Subject: [PATCH 1/3] add space-less version of postcode as alternative name --- .../model/PostcodeOldStyleRowMapper.java | 3 +-- .../nominatim/model/PostcodeRowMapper.java | 6 ++--- .../photon/nominatim/model/PostcodeUtils.java | 24 +++++++++++++++++ .../nominatim/NominatimConnectorDBTest.java | 27 +++++++++++++++++++ 4 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 src/main/java/de/komoot/photon/nominatim/model/PostcodeUtils.java diff --git a/src/main/java/de/komoot/photon/nominatim/model/PostcodeOldStyleRowMapper.java b/src/main/java/de/komoot/photon/nominatim/model/PostcodeOldStyleRowMapper.java index b0473d076..325d4001f 100644 --- a/src/main/java/de/komoot/photon/nominatim/model/PostcodeOldStyleRowMapper.java +++ b/src/main/java/de/komoot/photon/nominatim/model/PostcodeOldStyleRowMapper.java @@ -7,7 +7,6 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; -import java.util.Map; /** * Access to postcode tables prior to Nominatim 5.3. @@ -30,7 +29,7 @@ public PhotonDoc rowToDoc(ResultSet rs) throws SQLException { Long.toString(rs.getLong("place_id")), null, -1, "place", "postcode") - .names(NameMap.makeForPlace(Map.of("name", rs.getString("postcode")), List.of())) + .names(PostcodeUtils.postcodeToName(rs.getString("postcode"))) .centroid(centroid) .countryCode(rs.getString("country_code")) .categories(List.of("osm.place.postcode")) diff --git a/src/main/java/de/komoot/photon/nominatim/model/PostcodeRowMapper.java b/src/main/java/de/komoot/photon/nominatim/model/PostcodeRowMapper.java index abe9c9be4..f264b2ce9 100644 --- a/src/main/java/de/komoot/photon/nominatim/model/PostcodeRowMapper.java +++ b/src/main/java/de/komoot/photon/nominatim/model/PostcodeRowMapper.java @@ -3,18 +3,16 @@ import de.komoot.photon.PhotonDoc; import de.komoot.photon.nominatim.DBDataAdapter; import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; -import java.util.Map; import java.util.Objects; /** * Access to postcode tables for Nominatim 5.3+. *

- * This table is now the only source for postcodes nad may contain full + * This table is now the only source for postcodes and may contain full * geometries for postcode areas. */ @NullMarked @@ -35,7 +33,7 @@ public PhotonDoc rowToDoc(ResultSet rs) throws SQLException { osmId == null ? null : "R", osmId == null ? -1 : (Long) osmId, "place", "postcode") - .names(NameMap.makeForPlace(Map.of("name", rs.getString("postcode")), List.of())) + .names(PostcodeUtils.postcodeToName(rs.getString("postcode"))) .centroid(Objects.requireNonNull(dbutils.extractGeometry(rs, "centroid"))) .countryCode(rs.getString("country_code")) .categories(List.of("osm.place.postcode")) diff --git a/src/main/java/de/komoot/photon/nominatim/model/PostcodeUtils.java b/src/main/java/de/komoot/photon/nominatim/model/PostcodeUtils.java new file mode 100644 index 000000000..57a53b5e3 --- /dev/null +++ b/src/main/java/de/komoot/photon/nominatim/model/PostcodeUtils.java @@ -0,0 +1,24 @@ +package de.komoot.photon.nominatim.model; + +import org.jspecify.annotations.NullMarked; + +import java.util.List; +import java.util.Map; + +@NullMarked +public class PostcodeUtils { + private PostcodeUtils() {} + + static NameMap postcodeToName(String postcode) { + String shortPostcode = postcode.replaceAll("[ -]", ""); + if (shortPostcode.length() != postcode.length()) { + return NameMap.makeForPlace(Map.of( + "name", postcode, + "alt_name", shortPostcode + ), List.of()); + } + + return NameMap.makeForPlace(Map.of("name", postcode), List.of()); + } + +} diff --git a/src/test/java/de/komoot/photon/nominatim/NominatimConnectorDBTest.java b/src/test/java/de/komoot/photon/nominatim/NominatimConnectorDBTest.java index 6d0f82682..7fd1a02f9 100644 --- a/src/test/java/de/komoot/photon/nominatim/NominatimConnectorDBTest.java +++ b/src/test/java/de/komoot/photon/nominatim/NominatimConnectorDBTest.java @@ -537,6 +537,33 @@ void testPostcodeArea() { } + @Test + void testPostcodeWithSpaces() { + var parent = new PlacexTestRow("place", "city").name("Rio") + .ranks(16) + .country("nl") + .add(jdbc); + var postcode = new PostcodeLocationTestRow("33XV 46", "nl") + .geometry("POLYGON((-10.999 33.999, -11.001 34.001, -11.001 33.999, -10.999 33.999))") + .centroid(-11, 34) + .relation(55) + .parent(parent.getPlaceId()) + .add(jdbc); + + readEntireDatabase(); + + importer.assertThatByPlaceId(postcode.getPlaceString()) + .hasFieldOrPropertyWithValue("osmId", 55L) + .hasFieldOrPropertyWithValue("tagKey", "place") + .hasFieldOrPropertyWithValue("tagValue", "postcode") + .hasFieldOrPropertyWithValue("postcode", null) + .hasFieldOrPropertyWithValue("name", Map.of( + "default", "33XV 46", + "alt", "33XV46")) + ; + + } + @Test void testBoundariesHaveAdminLevelInExtraTags() { var place = new PlacexTestRow("boundary", "administrative").name("Rio") From 2e74cede11c9310b2a3be953627008107105ba81 Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Fri, 7 Aug 2026 10:38:21 +0200 Subject: [PATCH 2/3] use Nominatim's postcode field as only source of truth The field provides properly formatted postcodes and drops the ones that do not comply to the country's format. That should lead to better results. --- src/main/java/de/komoot/photon/PhotonDoc.java | 2 +- .../java/de/komoot/photon/nominatim/NominatimImporter.java | 3 +++ .../java/de/komoot/photon/nominatim/NominatimUpdater.java | 2 ++ .../de/komoot/photon/nominatim/model/OsmlineRowMapper.java | 3 +-- .../de/komoot/photon/nominatim/model/PlaceRowMapper.java | 3 +-- src/test/java/de/komoot/photon/json/JsonDumperTest.java | 1 - .../komoot/photon/nominatim/NominatimConnectorDBTest.java | 6 +++--- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/komoot/photon/PhotonDoc.java b/src/main/java/de/komoot/photon/PhotonDoc.java index 6d065d3ae..908709ff5 100644 --- a/src/main/java/de/komoot/photon/PhotonDoc.java +++ b/src/main/java/de/komoot/photon/PhotonDoc.java @@ -187,7 +187,7 @@ public PhotonDoc addressType(AddressType type) { return this; } - public PhotonDoc postcode(String postcode) { + public PhotonDoc postcode(@Nullable String postcode) { this.postcode = postcode; return this; } diff --git a/src/main/java/de/komoot/photon/nominatim/NominatimImporter.java b/src/main/java/de/komoot/photon/nominatim/NominatimImporter.java index 4147a7747..85a7b486e 100644 --- a/src/main/java/de/komoot/photon/nominatim/NominatimImporter.java +++ b/src/main/java/de/komoot/photon/nominatim/NominatimImporter.java @@ -73,6 +73,7 @@ public void readCountry(String countryCode, ImportThread importThread) { doc.addAddresses(addressCache.getAddressList(rs.getString("addresslines"))); doc.addAddresses(address, dbProperties.getLanguages()); // take precedence over computed address + doc.postcode(rs.getString("postcode")); // ignore all other sources of postcode doc.setCountry(cnames); importThread.addDocument(new PhotonDocAddressSet(doc, address)); @@ -101,6 +102,7 @@ public void readCountry(String countryCode, ImportThread importThread) { } doc.addAddresses(addressCache.getAddressList(rs.getString("addresslines"))); doc.addAddresses(address, dbProperties.getLanguages()); // take precedence over computed address + doc.postcode(rs.getString("postcode")); // ignore all other sources of postcode doc.setCountry(cnames); importThread.addDocument(new PhotonDocAddressSet(doc, address)); @@ -123,6 +125,7 @@ public void readCountry(String countryCode, ImportThread importThread) { } doc.addAddresses(addressCache.getAddressList(rs.getString("addresslines"))); doc.addAddresses(dbutils.getMap(rs, "address"), dbProperties.getLanguages()); + doc.postcode(rs.getString("postcode")); // ignore all other sources of postcode doc.setCountry(cnames); diff --git a/src/main/java/de/komoot/photon/nominatim/NominatimUpdater.java b/src/main/java/de/komoot/photon/nominatim/NominatimUpdater.java index 6af189c48..82f2b7779 100644 --- a/src/main/java/de/komoot/photon/nominatim/NominatimUpdater.java +++ b/src/main/java/de/komoot/photon/nominatim/NominatimUpdater.java @@ -119,6 +119,7 @@ public NominatimUpdater(PostgresqlConfig config, DBDataAdapter dataAdapter, Data // Add address last, so it takes precedence. final var address = dbutils.getMap(rs, "address"); doc.addAddresses(address, dbProperties.getLanguages()); + doc.postcode(rs.getString("postcode")); // ignore all other sources of postcode assert countryNames != null; doc.setCountry(countryNames.get(rs.getString("country_code"))); @@ -143,6 +144,7 @@ public NominatimUpdater(PostgresqlConfig config, DBDataAdapter dataAdapter, Data doc.addAddresses( addressCache.getOrLoadAddressList(template, rs.getString("addresslines"))); doc.addAddresses(dbutils.getMap(rs, "address"), dbProperties.getLanguages()); + doc.postcode(rs.getString("postcode")); // ignore all other sources of postcode assert countryNames != null; doc.setCountry(countryNames.get(rs.getString("country_code"))); diff --git a/src/main/java/de/komoot/photon/nominatim/model/OsmlineRowMapper.java b/src/main/java/de/komoot/photon/nominatim/model/OsmlineRowMapper.java index bcbd25521..5bddf12d3 100644 --- a/src/main/java/de/komoot/photon/nominatim/model/OsmlineRowMapper.java +++ b/src/main/java/de/komoot/photon/nominatim/model/OsmlineRowMapper.java @@ -19,8 +19,7 @@ public PhotonDoc mapRow(ResultSet rs, int rowNum) throws SQLException { "place", "house_number") .countryCode(rs.getString("country_code")) .categories(List.of("osm.place.house_number")) - .addressType(AddressType.HOUSE) - .postcode(rs.getString("postcode")); + .addressType(AddressType.HOUSE); } public String makeBaseQuery(DBDataAdapter dbutils) { diff --git a/src/main/java/de/komoot/photon/nominatim/model/PlaceRowMapper.java b/src/main/java/de/komoot/photon/nominatim/model/PlaceRowMapper.java index 098b0aa7a..cde92aefe 100644 --- a/src/main/java/de/komoot/photon/nominatim/model/PlaceRowMapper.java +++ b/src/main/java/de/komoot/photon/nominatim/model/PlaceRowMapper.java @@ -69,8 +69,7 @@ public PhotonDoc mapRow(ResultSet rs, int rowNum) throws SQLException { .bbox(dbutils.extractGeometry(rs, "bbox")) .countryCode(rs.getString("country_code")) .centroid(Objects.requireNonNull(dbutils.extractGeometry(rs, "centroid"))) - .addressType(addressType) - .postcode(rs.getString("postcode")); + .addressType(addressType); if (useGeometryColumn) { try { diff --git a/src/test/java/de/komoot/photon/json/JsonDumperTest.java b/src/test/java/de/komoot/photon/json/JsonDumperTest.java index 4ac6c55fa..4963b175c 100644 --- a/src/test/java/de/komoot/photon/json/JsonDumperTest.java +++ b/src/test/java/de/komoot/photon/json/JsonDumperTest.java @@ -17,7 +17,6 @@ import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.transaction.support.TransactionTemplate; diff --git a/src/test/java/de/komoot/photon/nominatim/NominatimConnectorDBTest.java b/src/test/java/de/komoot/photon/nominatim/NominatimConnectorDBTest.java index 7fd1a02f9..55785ae93 100644 --- a/src/test/java/de/komoot/photon/nominatim/NominatimConnectorDBTest.java +++ b/src/test/java/de/komoot/photon/nominatim/NominatimConnectorDBTest.java @@ -478,12 +478,12 @@ void testUsePostcodeFromPlacex() { } @Test - void testPreferPostcodeFromAddress() { + void testPreferPostcodeFromPostcodeField() { PlacexTestRow parent = PlacexTestRow.make_street("Main St").add(jdbc); PlacexTestRow place = new PlacexTestRow("building", "yes") .addr("housenumber", "34") - .addr("postcode", "45-234") - .postcode("XXX") + .addr("postcode", "XXX") + .postcode("45-234") .parent(parent).add(jdbc); PlacexTestRow postcode = new PlacexTestRow("boundary", "postal_code") .name("ref", "1234XZ").ranks(11).add(jdbc); From 3e47884db315fe1f51bf9a9e335809643b6f4ff7 Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Fri, 7 Aug 2026 14:14:33 +0200 Subject: [PATCH 3/3] make postcocde without spaces searchable --- .../komoot/photon/nominatim/model/PostcodeUtils.java | 11 +++++++++-- .../komoot/photon/opensearch/PhotonDocSerializer.java | 5 +++++ .../java/de/komoot/photon/searcher/QueryReranker.java | 7 ++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/komoot/photon/nominatim/model/PostcodeUtils.java b/src/main/java/de/komoot/photon/nominatim/model/PostcodeUtils.java index 57a53b5e3..2859042d0 100644 --- a/src/main/java/de/komoot/photon/nominatim/model/PostcodeUtils.java +++ b/src/main/java/de/komoot/photon/nominatim/model/PostcodeUtils.java @@ -1,6 +1,7 @@ package de.komoot.photon.nominatim.model; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.util.List; import java.util.Map; @@ -9,9 +10,15 @@ public class PostcodeUtils { private PostcodeUtils() {} - static NameMap postcodeToName(String postcode) { + public static @Nullable String postcodeAltName(String postcode) { String shortPostcode = postcode.replaceAll("[ -]", ""); - if (shortPostcode.length() != postcode.length()) { + + return shortPostcode.length() == postcode.length() ? null : shortPostcode; + } + + public static NameMap postcodeToName(String postcode) { + String shortPostcode = postcodeAltName(postcode); + if (shortPostcode != null) { return NameMap.makeForPlace(Map.of( "name", postcode, "alt_name", shortPostcode diff --git a/src/main/java/de/komoot/photon/opensearch/PhotonDocSerializer.java b/src/main/java/de/komoot/photon/opensearch/PhotonDocSerializer.java index 01cb69908..bdab6dbb7 100644 --- a/src/main/java/de/komoot/photon/opensearch/PhotonDocSerializer.java +++ b/src/main/java/de/komoot/photon/opensearch/PhotonDocSerializer.java @@ -6,6 +6,7 @@ import de.komoot.photon.DatabaseProperties; import de.komoot.photon.PhotonDoc; import de.komoot.photon.nominatim.model.AddressType; +import de.komoot.photon.nominatim.model.PostcodeUtils; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import org.locationtech.jts.geom.Envelope; @@ -61,6 +62,10 @@ public void serialize(PhotonDoc value, JsonGenerator gen, SerializerProvider pro if (value.getPostcode() != null) { gen.writeStringField(DocFields.POSTCODE, value.getPostcode()); termCollector.add(value.getPostcode(), 2); + var altPostcode = PostcodeUtils.postcodeAltName(value.getPostcode()); + if (altPostcode != null) { + termCollector.add(altPostcode, 2); + } } if (isNamed) { diff --git a/src/main/java/de/komoot/photon/searcher/QueryReranker.java b/src/main/java/de/komoot/photon/searcher/QueryReranker.java index 93ac049ce..c4be64596 100644 --- a/src/main/java/de/komoot/photon/searcher/QueryReranker.java +++ b/src/main/java/de/komoot/photon/searcher/QueryReranker.java @@ -1,5 +1,6 @@ package de.komoot.photon.searcher; +import de.komoot.photon.nominatim.model.PostcodeUtils; import de.komoot.photon.opensearch.DocFields; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -103,7 +104,11 @@ private double rescore(PhotonResult result) { mapNames(result.getLocalised(DocFields.STATE, language), resultTerms); mapNames(result.getLocalised(DocFields.COUNTY, language), resultTerms); mapNames(result.getLocalised(DocFields.DISTRICT, language), resultTerms); - mapNames((String) result.get(DocFields.POSTCODE), resultTerms); + var postcode = (String) result.get(DocFields.POSTCODE); + if (postcode != null) { + mapNames(postcode, resultTerms); + mapNames(PostcodeUtils.postcodeAltName(postcode), resultTerms); + } mapNames(result.getLocalised(DocFields.CITY, "default"), resultTerms); mapNames(result.getLocalised(DocFields.STATE, "default"), resultTerms); mapNames(result.getLocalised(DocFields.COUNTY, "default"), resultTerms);