Zoeken naar een woning is een ruimtelijk vraagstuk. Kopers en huurders denken in wijken, reistijden naar hun werk en de afstand tot scholen, niet in postcodes en straatnamen. Een interface waarin de kaart centraal staat, sluit aan bij hoe mensen zoeken. Gebruikers bakenen hun zoekgebied dan eerst op de kaart af, en filteren pas daarna op prijs, aantal slaapkamers of andere kenmerken.
In deze tutorial bouw je een kaart met woningaanbod die klaar is voor productie: geclusterde markers bij lage zoom, prijslabels op losse markers, een popup met details bij een klik, een zoekfunctie op postcode die de kaart opnieuw centreert en een prijsslider die het zichtbare aanbod in realtime filtert. Het React-component zelf is ongeveer 65 regels, en de volledige opbouw met lagen en klikhandlers komt uit op zo'n 150. Dezelfde logica werkt ook in vanilla JavaScript als je liever geen framework gebruikt.
Je vindt ook een toelichting op de AVG-verplichtingen voor PropTech-platforms in de EU. De locatiedata die bij het zoeken naar woningen ontstaat, is namelijk persoonsgegevens en moet ook zo worden behandeld.
Ben je nieuw met kaart-API's? De tutorial How to Add Interactive Maps to Your Website behandelt eerst de basis, waarna deze tutorial verdergaat.
De datastructuur van het woningaanbod
Elke woning heeft coördinaten, een prijs en genoeg metadata voor de popup nodig. Houd dit bij als een GeoJSON FeatureCollection, dan past het zonder conversie direct in de kaartbron.
const properties = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.8952, 52.3702] },
properties: {
id: "prop-001",
price: 485000,
bedrooms: 3,
sqm: 112,
address: "Keizersgracht 142, Amsterdam",
type: "apartment",
status: "for-sale"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9123, 52.3601] },
properties: {
id: "prop-002",
price: 1250,
bedrooms: 2,
sqm: 78,
address: "Sarphatistraat 58, Amsterdam",
type: "apartment",
status: "for-rent"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.8801, 52.3780] },
properties: {
id: "prop-003",
price: 720000,
bedrooms: 4,
sqm: 195,
address: "Herengracht 380, Amsterdam",
type: "house",
status: "for-sale"
}
}
]
};
Op een echt platform komt deze array uit je listings-API, via een fetch-aanroep bij het laden van de kaart of bij een wijziging in de queryparameters.
De kaart opzetten
Initialiseer de kaart met een centraal beeld van je woningmarkt. De MapAtlas Maps API is compatibel met Mapbox GL JS, dus de initialisatie is identiek aan wat je voor Mapbox zou schrijven, alleen met een tile-URL van MapAtlas.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'map',
style: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 13
});
Een lichte stijl met weinig contrast werkt bijzonder goed voor vastgoedkaarten: door de lichte basis vallen prijslabels en gekleurde markers op zonder visuele rommel.
Clustering met prijslabels
Het belangrijkste UI-patroon op een vastgoedkaart: prijslabels op losse markers en tellers op clustercirkels. Met de bronoptie cluster: true worden punten die dicht bij elkaar liggen automatisch gegroepeerd. Voor clusters en losse markers voeg je aparte lagen toe.
map.on('load', () => {
map.addSource('properties', {
type: 'geojson',
data: properties,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 60
});
// Clustercirkels, grootte schaalt mee met het aantal woningen
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'properties',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#2563EB',
'circle-radius': [
'step', ['get', 'point_count'],
22, 5, 30, 20, 38
],
'circle-stroke-width': 3,
'circle-stroke-color': '#ffffff'
}
});
// Labels met het aantal per cluster
map.addLayer({
id: 'cluster-label',
type: 'symbol',
source: 'properties',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 14
},
paint: { 'text-color': '#ffffff' }
});
// Losse woningmarkers, witte cirkel met prijstekst
map.addLayer({
id: 'property-price',
type: 'symbol',
source: 'properties',
filter: ['!', ['has', 'point_count']],
layout: {
'text-field': [
'concat',
'€',
['to-string', ['round', ['/', ['get', 'price'], 1000]]],
'k'
],
'text-font': ['DIN Offc Pro Bold', 'Arial Unicode MS Bold'],
'text-size': 12
},
paint: {
'text-color': '#1e293b',
'text-halo-color': '#ffffff',
'text-halo-width': 2
}
});
});
Klikinteracties
Klik je op een cluster, dan zoomt de kaart in zodat de afzonderlijke woningen zichtbaar worden. Klik je op een losse woning, dan opent een popup met details.
// Inzoomen op het aangeklikte cluster
map.on('click', 'clusters', (e) => {
const [feature] = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
map.getSource('properties').getClusterExpansionZoom(
feature.properties.cluster_id,
(err, zoom) => {
if (!err) map.easeTo({ center: feature.geometry.coordinates, zoom });
}
);
});
// Popup met details voor een losse woning
map.on('click', 'property-price', (e) => {
const { address, price, bedrooms, sqm, status, id } = e.features[0].properties;
const coords = e.features[0].geometry.coordinates.slice();
const formattedPrice = status === 'for-rent'
? `€${price.toLocaleString()}/mo`
: `€${price.toLocaleString()}`;
new mapmetricsgl.Popup({ offset: 10 })
.setLngLat(coords)
.setHTML(`
<div style="min-width:200px">
<strong style="font-size:15px">${formattedPrice}</strong>
<p style="margin:4px 0">${address}</p>
<p style="margin:4px 0;color:#64748b">${bedrooms} bed · ${sqm} m²</p>
<a href="/listings/${id}" style="color:#2563EB;font-weight:600">View listing →</a>
</div>
`)
.addTo(map);
});
// Cursor aanpassen bij hover
map.on('mouseenter', 'property-price', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'property-price', () => { map.getCanvas().style.cursor = ''; });
map.on('mouseenter', 'clusters', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'clusters', () => { map.getCanvas().style.cursor = ''; });
Filteren op prijsklasse
Een slider die het zichtbare aanbod filtert, is een van de waardevolste UI-toevoegingen voor een vastgoedkaart. Met het expressiesysteem van Mapbox GL JS pas je het filter aan de clientkant aan, zonder netwerkverzoek. Het filteren gebeurt in de browser op de GeoJSON die al geladen is.
function applyPriceFilter(min, max) {
const filter = ['all',
['!', ['has', 'point_count']],
['>=', ['get', 'price'], min],
['<=', ['get', 'price'], max]
];
map.setFilter('property-price', filter);
}
// Range-inputs koppelen
const minInput = document.getElementById('price-min');
const maxInput = document.getElementById('price-max');
function onRangeChange() {
applyPriceFilter(Number(minInput.value), Number(maxInput.value));
}
minInput.addEventListener('input', onRangeChange);
maxInput.addEventListener('input', onRangeChange);
Let op: clusters worden berekend uit de brondata, niet uit laagfilters. De clusteraantallen tellen dus nog steeds woningen mee die de slider heeft verborgen. Houd ze synchroon door het aanbod in JavaScript te filteren en het resultaat door te geven aan map.getSource('properties').setData().
Zoeken op adres met de Geocoding API
Laat gebruikers een wijk of postcode intypen om de kaart opnieuw te centreren. De Geocoding API geeft GeoJSON-features terug, zodat de coördinaten direct in map.flyTo kunnen.
async function searchArea(query) {
const url = new URL('https://gateway.mapmetrics-atlas.net/forward-geocode/');
url.searchParams.set('text', query);
url.searchParams.set('token', 'YOUR_API_KEY');
url.searchParams.set('size', '1');
const res = await fetch(url);
const data = await res.json();
if (!data.features.length) return;
const [lng, lat] = data.features[0].geometry.coordinates;
map.flyTo({ center: [lng, lat], zoom: 13 });
}
document.getElementById('area-search').addEventListener('keydown', (e) => {
if (e.key === 'Enter') searchArea(e.target.value.trim());
});
Wil je zoeken op reistijd, zoals "toon woningen binnen 20 minuten van deze school"? Voeg dan een isochroon-overlay toe met de MapAtlas Travel Time API. Het artikel Isochrone Maps Explained laat zien hoe je die polygoon ophaalt en weergeeft.
Het complete React-component
Alles hierboven samengevoegd in een React-component met correct lifecyclebeheer:
import { useEffect, useRef, useState } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
export function PropertyMap({ listings, apiKey }) {
const containerRef = useRef(null);
const mapRef = useRef(null);
const [priceRange, setPriceRange] = useState([0, 2000000]);
useEffect(() => {
const map = new mapmetricsgl.Map({
container: containerRef.current,
style: `https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=${apiKey}`,
center: [4.9041, 52.3676],
zoom: 13
});
mapRef.current = map;
map.on('load', () => {
map.addSource('properties', {
type: 'geojson',
data: { type: 'FeatureCollection', features: listings },
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 60
});
// Voeg de lagen cluster, cluster-label en property-price toe (zie hierboven)
// Voeg de klikhandlers toe (zie hierboven)
});
return () => map.remove();
}, [apiKey]);
// Filter bijwerken als de prijsslider verandert
useEffect(() => {
const map = mapRef.current;
if (!map || !map.getLayer('property-price')) return;
map.setFilter('property-price', [
'all',
['!', ['has', 'point_count']],
['>=', ['get', 'price'], priceRange[0]],
['<=', ['get', 'price'], priceRange[1]]
]);
}, [priceRange]);
return (
<div>
<div style={{ padding: '12px 0', display: 'flex', gap: 12 }}>
<label>
Min €
<input type="range" min={0} max={2000000} step={10000}
value={priceRange[0]}
onChange={e => setPriceRange([+e.target.value, priceRange[1]])} />
</label>
<label>
Max €
<input type="range" min={0} max={2000000} step={10000}
value={priceRange[1]}
onChange={e => setPriceRange([priceRange[0], +e.target.value])} />
</label>
</div>
<div ref={containerRef} style={{ width: '100%', height: '600px' }} />
</div>
);
}
Dat is het basisframe van het component: ongeveer 65 regels, inclusief de prijsslider en het opruimen bij unmount. Voeg de code voor lagen en klikhandlers uit de secties hierboven toe om het compleet te maken.
AVG-aandachtspunten voor PropTech in de EU
Typt een woningzoeker zijn thuisadres in of klikt hij op "gebruik mijn locatie" op je kaart, dan deelt hij persoonsgegevens in de zin van artikel 4, lid 1, van de AVG, dat locatiegegevens noemt als identificator. Die locatiedata laat zien waar iemand woont, en daaruit zijn mogelijk andere gevoelige kenmerken af te leiden.
Praktische stappen om aan de EU-regels te voldoen:
- Stuur geocodingverzoeken via je backend in plaats van de Geocoding API direct vanuit de browser aan te roepen. Je backend kan identificerende headers verwijderen voordat het verzoek wordt doorgestuurd.
- Log of bewaar zoekcoördinaten niet langer dan de sessie, tenzij je een gedocumenteerde rechtsgrond hebt onder artikel 6 van de AVG, zoals toestemming.
- Bied je een functie als "laat het me weten als een woning in de buurt aan mijn criteria voldoet"? Behandel het opgeslagen zoekgebied dan als persoonsgegevens met een vastgestelde bewaartermijn.
De EU Developer's Guide to GDPR-Compliant Map APIs behandelt dit uitgebreid, inclusief verwerkersovereenkomsten en eisen aan dataresidentie. MapAtlas verwerkt data binnen de EU en levert de verwerkersovereenkomst die artikel 28, lid 3, van de AVG vereist tussen verwerkingsverantwoordelijke en verwerker.
Op de oplossingspagina voor de vastgoedsector lees je meer over hoe PropTech-platforms MapAtlas in productie gebruiken.
Volgende stappen
- Maak een gratis MapAtlas API-sleutel aan en begin met het gratis abonnement
- Voeg reistijd-overlays toe met de Travel Time API en laat kopers zien wat binnen 20 minuten bereikbaar is
- Bekijk de stylinggids voor de Maps API om de kaart af te stemmen op de huisstijl van je vastgoedplatform
Veelgestelde vragen
Kan ik MapAtlas gebruiken voor een website met woningaanbod?
Ja. MapAtlas dekt wat vastgoedsites nodig hebben: zoekkaarten met woningen, investeringsdashboards en kaarten met huuraanbod. De Maps API is compatibel met Mapbox GL JS, dus bestaande code voor vastgoedkaarten op basis van Mapbox migreer je met minimale aanpassingen.
Hoe werkt markerclustering op vastgoedkaarten?
Clustering groepeert woningmarkers die dicht bij elkaar liggen tot één cirkel met het aantal, zolang de kaart is uitgezoomd. Zoomt de gebruiker in, dan splitsen de clusters zich op in losse markers. MapAtlas ondersteunt clustering van GeoJSON-bronnen standaard: zet cluster: true op de bron en de SDK regelt de rest.
Valt locatiedata van woningzoekers onder de AVG?
Ja. Zoekt een gebruiker op een vastgoedsite op zijn thuisadres of huidige locatie, dan zijn dat persoonsgegevens in de zin van artikel 4 van de AVG. PropTech-platforms in de EU horen geocodingverzoeken via een backend-proxy te laten lopen en passende regels voor dataminimalisatie en bewaartermijnen toe te passen. Doe geen API-aanroepen vanaf de client die locaties van gebruikers laten vastleggen op servers van derden.
Veelgestelde vragen
Kan ik MapAtlas gebruiken voor een website met woningaanbod?
Ja. MapAtlas dekt wat vastgoedsites nodig hebben: zoekkaarten met woningen, investeringsdashboards en kaarten met huuraanbod. De Maps API is compatibel met Mapbox GL JS, dus bestaande code voor vastgoedkaarten op basis van Mapbox migreer je met minimale aanpassingen.
Hoe werkt markerclustering op vastgoedkaarten?
Clustering groepeert woningmarkers die dicht bij elkaar liggen tot één cirkel met het aantal, zolang de kaart is uitgezoomd. Zoomt de gebruiker in, dan splitsen de clusters zich op in losse markers. MapAtlas ondersteunt clustering van GeoJSON-bronnen standaard: zet `cluster: true` op de bron en de SDK regelt de rest.
Valt locatiedata van woningzoekers onder de AVG?
Ja. Zoekt een gebruiker op een vastgoedsite op zijn thuisadres of huidige locatie, dan zijn dat persoonsgegevens in de zin van artikel 4 van de AVG. PropTech-platforms in de EU horen geocodingverzoeken via een backend-proxy te laten lopen en passende regels voor dataminimalisatie en bewaartermijnen toe te passen. Doe geen API-aanroepen vanaf de client die locaties van gebruikers laten vastleggen op servers van derden.

