Repository navigation
Constraining to locality is dangerous in Belgium #393
Description
Bug description
In Belgium the same postal code can have multiple locality names. For example, 3071 is in "Erps-Kwerps", which is a subcommune of "Kortenberg" (3070). Note that subcommunes are not geographically nested - so 3071 refers to a region adjacent to 3070, not inside 3070.
For some reason, Pelias enriches an address in 3071 with the locality name of 3070, so you get
postalcode = 3071
locality = Kortenberg
localadmin = Kortenberg
Steps to reproduce
As a result, a query for "mechelsesteenweg 40 3071 erps-kwerps belgië" returns just the centroid of erps-kwerps
{
"geocoding": {
"version": "0.2",
"attribution": "http://localhost:4000/attribution",
"query": {
"text": "mechelsesteenweg 40 3071 erps-kwerps belgi\u00eb",
"size": 5,
"layers": [
"address",
"street"
],
"private": false,
"focus.point.lat": 50.85,
"focus.point.lon": 4.35,
"lang": {
"name": "English",
"iso6391": "en",
"iso6393": "eng",
"via": "querystring",
"defaulted": false
},
"querySize": 20,
"parser": "libpostal",
"parsed_text": {
"street": "mechelsesteenweg",
"housenumber": "40",
"postalcode": "3071",
"city": "erps-kwerps",
"country": "belgi\u00eb"
}
},
"engine": {
"name": "Pelias",
"author": "Mapzen",
"version": "1.0"
},
"timestamp": 1790841862795
},
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
4.551228,
50.897843
]
},
"properties": {
"id": "101758525",
"gid": "whosonfirst:locality:101758525",
"layer": "locality",
"source": "whosonfirst",
"source_id": "101758525",
"country_code": "BE",
"name": "Erps-Kwerps",
"confidence": 0.6,
"match_type": "fallback",
"distance": 15.105,
"accuracy": "centroid",
"country": "Belgium",
"country_gid": "whosonfirst:country:85632997",
"country_a": "BEL",
"macroregion": "Flemish Region",
"macroregion_gid": "whosonfirst:macroregion:404227357",
"region": "Vlaams-Brabant",
"region_gid": "whosonfirst:region:85681731",
"region_a": "VB",
"county": "Leuven",
"county_gid": "whosonfirst:county:102049969",
"county_a": "LV",
"localadmin": "Kortenberg",
"localadmin_gid": "whosonfirst:localadmin:1108830227",
"locality": "Erps-Kwerps",
"locality_gid": "whosonfirst:locality:101758525",
"label": "Erps-Kwerps, VB, Belgium"
},
"bbox": [
4.540485,
50.89283,
4.594644,
50.913749
]
}
],
"bbox": [
4.540485,
50.89283,
4.594644,
50.913749
]
}
because libpostal identies "Erps-Kwerps" as the locality and pelias constrains its search to addresses in "Erps-Kwerps". The actual address however has "Kortenberg" as the locality.
You can verify this by searching for the address without the commune name: mechelsesteenweg 40 3071 belgië which returns the correct address
{
"geocoding": {
"version": "0.2",
"attribution": "http://localhost:4000/attribution",
"query": {
"text": "mechelsesteenweg 40 3071 belgi\u00eb",
"size": 1,
"private": false,
"focus.point.lat": 50.85,
"focus.point.lon": 4.35,
"lang": {
"name": "English",
"iso6391": "en",
"iso6393": "eng",
"via": "querystring",
"defaulted": false
},
"querySize": 20,
"parser": "libpostal",
"parsed_text": {
"street": "mechelsesteenweg",
"housenumber": "40",
"postalcode": "3071",
"country": "belgi\u00eb"
}
},
"engine": {
"name": "Pelias",
"author": "Mapzen",
"version": "1.0"
},
"timestamp": 1790842826662
},
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
4.53923,
50.89358
]
},
"properties": {
"id": "be/vlg/bosa-region-flanders-nl:f7d20a06e0757739",
"gid": "openaddresses:address:be/vlg/bosa-region-flanders-nl:f7d20a06e0757739",
"layer": "address",
"source": "openaddresses",
"source_id": "be/vlg/bosa-region-flanders-nl:f7d20a06e0757739",
"country_code": "BE",
"name": "Mechelsesteenweg 40",
"housenumber": "40",
"street": "Mechelsesteenweg",
"postalcode": "3071",
"confidence": 1,
"match_type": "exact",
"distance": 14.151,
"accuracy": "point",
"country": "Belgium",
"country_gid": "whosonfirst:country:85632997",
"country_a": "BEL",
"macroregion": "Flemish Region",
"macroregion_gid": "whosonfirst:macroregion:404227357",
"region": "Vlaams-Brabant",
"region_gid": "whosonfirst:region:85681731",
"region_a": "VB",
"county": "Leuven",
"county_gid": "whosonfirst:county:102049969",
"county_a": "LV",
"localadmin": "Kortenberg",
"localadmin_gid": "whosonfirst:localadmin:1108830227",
"locality": "Kortenberg",
"locality_gid": "whosonfirst:locality:101754327",
"label": "Mechelsesteenweg 40, Kortenberg, VB, Belgium"
}
}
],
"bbox": [
4.53923,
50.89358,
4.53923,
50.89358
]
}
For what it's worth, this particular case can be solved by adding a "does not have postcode" guard around the granularity_bands code, but I don't know enough about the pelias flow to know if that breaks something else (ChatGPT generated code below). An ideal solution I think would check the granularity of the output before moving on. E.g. finding a city inside the constraint is not likely to be favoured over finding an address in a looser constraint?
// If a postcode is present, don't constrain address matching by
// Placeholder-derived administrative IDs.
//
// Postal codes are a stronger discriminator for address matching and
// admin hierarchies can disagree about locality vs localadmin
// (e.g. Belgian deelgemeente vs municipality).
const hasPostcode = !_.isEmpty(clean.parsed_text.postalcode);
if (!hasPostcode) {
// find the first granularity band for which there are results
let granularity_band = granularity_bands.find(
band => anyResultsAtGranularityBand(results, band)
);
// special case: if locality is present in the band, also include macrocounty
if (granularity_band && granularity_band.includes('locality')) {
granularity_band = [...granularity_band, 'macrocounty'];
}
if (granularity_band) {
const layers_to_ids = granularity_band.reduce((acc, layer) => {
acc[layer] = getIdsAtLayer(res.data, layer);
return acc;
}, {});
vs.var('input:layers', layers_to_ids);
}
}
Github references
Maybe related to #61 ?
Or pelias/pelias#396 ?
Environment
{
"logger": {
"level": "debug",
"timestamp": false
},
"esclient": {
"apiVersion": "7.5",
"hosts": [
{ "host": "elasticsearch" }
]
},
"elasticsearch": {
"settings": {
"index": {
"refresh_interval": "10s",
"number_of_replicas": "0",
"number_of_shards": "1"
}
}
},
"api": {
"services": {
"libpostal": { "url": "http://libpostal:4400" },
"placeholder": { "url": "http://placeholder:4100" },
"interpolation": { "url": "http://interpolation:4300" }
},
"defaultParameters": {
"focus.point.lat": 50.85,
"focus.point.lon": 4.35
}
},
"imports": {
"adminLookup": {
"enabled": true
},
"geonames": {
"datapath": "/data/geonames",
"countryCode": "BE"
},
"openstreetmap": {
"download": [
{ "sourceURL": "http://download.geofabrik.de/europe/belgium-latest.osm.pbf" }
],
"leveldbpath": "/tmp",
"datapath": "/data/openstreetmap",
"import": [{
"filename": "belgium-latest.osm.pbf"
}]
},
"openaddresses": {
"datapath": "/data/openaddresses",
"files": ["be/bru/bosa-region-brussels-nl.csv", "be/bru/bosa-region-brussels-fr.csv", "be/vlg/bosa-region-flanders-nl.csv", "be/wal/bosa-region-wallonia-fr.csv"]
},
"polyline": {
"datapath": "/data/polylines",
"files": [ "extract.0sv" ]
},
"whosonfirst": {
"datapath": "/data/whosonfirst",
"countryCode": "BE",
"importPostalcodes": true,
"importPlace": [ "85632997" ]
}
}
}
Log output
Data references
No response
Project or organization
No response
Activity
Danferno commentedon Oct 1, 2026
As a piece of inspiration (not familiar enough with the code/lang to properly validated), this is what Gpt-Sol suggests as a "double fallback" approach. It is supposed to run the regular search first, then try again without the locality constraint (if provided with street, housenumber and postalcode) if no address is found
patch-v1.js
address_search_without_admin_ids.js