Voseno
Terug naar Blog
Engineering
8 min leestijd

Hoe wij listings dedupliceren over 1.400 bronnen

Dezelfde Nederlandse woning verschijnt tegelijkertijd op Funda, Pararius, makelaarssites en aggregatoren. Dit is onze drie-signaal aanpak: BAG ID-resolutie, fuzzy adresnormalisatie en feature fingerprinting.

Wanneer een Nederlandse makelaar een woning aanbiedt, verschijnt deze doorgaans binnen enkele uren op meerdere portalen. Een huurappartement in Amsterdam kan opduiken op Funda, Pararius, de eigen website van de makelaar en twee of drie aggregatorsites, elk met de data net iets anders gepresenteerd. Voor onze Aggregator API, die meer dan 1.400 bronnen monitort, is het detecteren dat dit allemaal dezelfde woning is de belangrijkste technische uitdaging.

Waarom simpele matching faalt

De naïeve aanpak is exacte string-matching op het adres. Dit gaat in de praktijk direct mis. De ene bron vermeldt 'Keizersgracht 100-3', een andere toont 'Keizersgracht 100 III' en een derde gebruikt 'Keizersgracht 100 3 hoog.' Dit is allemaal hetzelfde appartement. Nederlandse adresconventies omvatten numerieke toevoegingen, lettertoevoegingen en beschrijvende toevoegingen (souterrain, boven, 3-hoog), en er is geen standaardformaat over portalen heen.

Prijzen variëren ook tussen bronnen. Het ene portaal toont de maandhuur inclusief servicekosten, het andere exclusief. Oppervlakten worden anders afgerond. Zelfs woningtypes zijn inconsistent: de ene bron noemt het een 'appartement,' de andere een 'bovenwoning.'

Signaal 1: BAG ID-resolutie

Het sterkste deduplicatiesignaal is het BAG verblijfsobject-ID. Elk Nederlands adres verwijst naar een uniek 16-cijferig identificatienummer in de Basisregistratie Adressen en Gebouwen. Wanneer we het adres van een listing kunnen koppelen aan het BAG ID via de PDOK Locatieserver, hebben we een definitieve match. Twee listings met hetzelfde BAG ID en listingtype (koop of huur) zijn dezelfde woning.

Dit werkt voor ruwweg 70% van de binnenkomende listings. Het faalt wanneer het listingadres onvolledig is (alleen straatnaam en stad, geen huisnummer), wanneer het adres een niet-standaard toevoeging bevat die PDOK niet kan oplossen, of wanneer de woning te nieuw is om al in BAG te staan.

Signaal 2: fuzzy adresnormalisatie

Voor listings waar BAG ID-resolutie niet lukt, normaliseren we het adres met Nederland-specifieke regels en vergelijken vervolgens met bewerkingsafstand.

python
import re
import unicodedata
from Levenshtein import distance

ABBREVIATIONS = {
    r"\bstr\.?\b": "straat",
    r"\bln\.?\b": "laan",
    r"\bpl\.?\b": "plein",
    r"\bgr\.?\b": "gracht",
    r"\bwg\.?\b": "weg",
}

def normalize_dutch_address(raw: str) -> str:
    """Normaliseer een Nederlands adres voor vergelijking."""
    addr = raw.lower().strip()
    for pattern, replacement in ABBREVIATIONS.items():
        addr = re.sub(pattern, replacement, addr)
    addr = re.sub(r"(\d+)\s*-\s*([a-z])", r"\1 \2", addr)
    addr = re.sub(r"\biii\b", "3", addr)
    addr = re.sub(r"\bii\b", "2", addr)
    addr = re.sub(r"\bi\b", "1", addr)
    addr = re.sub(r"\bhoog\b", "h", addr)
    addr = re.sub(r"\bboven\b", "h", addr)
    addr = unicodedata.normalize("NFKD", addr)
    addr = addr.encode("ascii", "ignore").decode("ascii")
    return re.sub(r"\s+", " ", addr).strip()

def addresses_match(a: str, b: str, threshold: int = 2) -> bool:
    """Controleer of twee adressen naar dezelfde locatie verwijzen."""
    return distance(
        normalize_dutch_address(a),
        normalize_dutch_address(b)
    ) <= threshold

De normalisatie verwerkt de meest voorkomende Nederlandse adresvariaties: afkortingen (str. naar straat, ln. naar laan), scheidingstekens bij huisnummertoevoegingen (100-A vs 100 A), Romeinse cijfers (III naar 3) en beschrijvende verdiepingsindicatoren (3-hoog, boven). Na normalisatie passen we Levenshtein-afstand toe met een drempel van 2 bewerkingen.

Signaal 3: feature fingerprinting

Wanneer adresmatching ambigu is, vooral in appartementsgebouwen waar listings soms het specifieke woningnummer weglaten, gebruiken we een feature-vingerafdruk: de combinatie van oppervlakte (binnen 3 m² tolerantie), prijsklasse en aantal kamers. In dichte Nederlandse woningmarkten is deze combinatie vrijwel uniek per listing binnen een gebouw.

Bijvoorbeeld: als twee listings beide 'Prinsengracht 200, Amsterdam' vermelden zonder woningnummer, maar de ene is 65 m², 3 kamers, €1.850/maand en de andere is 42 m², 2 kamers, €1.450/maand, dan zijn het duidelijk verschillende appartementen in hetzelfde gebouw. Als de kenmerken binnen de tolerantie overeenkomen, gaat het vrijwel zeker om dezelfde listing uit verschillende bronnen.

Lastige gevallen

Meerdere patronen blijven oprecht moeilijk. VvE-adressen (Vereniging van Eigenaars) gebruiken soms het gebouwadres in plaats van het individuele woningadres. Nieuwbouwwoningen staan mogelijk nog niet in BAG. Tijdelijke listings die kort op één bron verschijnen en vervolgens met andere details op een andere bron worden aangeboden, kunnen aan tijdvenster-matching ontsnappen.

Nieuwbouw met meerdere woningen is bijzonder lastig. Een ontwikkelaar kan '12 appartementen in Nieuwbouwproject De Werf' op één portaal aanbieden, terwijl individuele woningen op makelaarssites verschijnen met voorlopige adressen. Totdat de BAG-registratie bijgewerkt is, vereist dit handmatige beoordeling.

Info

Ons beleid: wanneer het vertrouwen onder 0,70 ligt, behandelen we listings als afzonderlijk. We tonen liever een mild duplicaat dan dat we twee afzonderlijke woningen onterecht samenvoegen.

Nauwkeurigheid meten

We onderhouden een handmatig gelabelde ground-truth dataset van 12.000 listingparen (6.000 bevestigde duplicaten en 6.000 bevestigde afzonderlijke woningen), samengesteld over zes maanden handmatige beoordeling. Tegen deze dataset behaalt onze pipeline 99,1% precisie en 98,8% recall. We breiden deze dataset continu uit naarmate we nieuwe randgevallen tegenkomen.