Skip to content

Constraining to locality is dangerous in Belgium #393

Open

Description

@Danferno

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

  1. Danferno commented on Oct 1, 2026

    @Danferno
    Author

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions