La recherche immobilière est un problème spatial. Les acheteurs et les locataires pensent en termes de quartiers, de temps de trajet et de proximité des écoles, pas de codes postaux et de listes de rues. Une interface axée sur la carte convertit mieux qu'une interface axée sur la liste, car elle permet aux utilisateurs d'ancrer leur recherche dans l'espace avant de filtrer par prix, nombre de chambres ou tout autre attribut.
Ce tutoriel construit une carte d'annonces immobilières prête pour la production : marqueurs regroupés à faible zoom, étiquettes de prix sur les marqueurs individuels, clic pour ouvrir une popup avec les détails de l'annonce, une recherche de code postal qui recentre la carte, et un curseur de plage de prix qui filtre les annonces visibles en temps réel. Le composant React complet tient en moins de 100 lignes. La même logique fonctionne en JavaScript vanilla si vous préférez sans framework.
Vous trouverez également une note sur les obligations RGPD spécifiques aux plateformes PropTech de l'UE, car les données de localisation générées par les recherches immobilières sont des données personnelles et doivent être traitées en conséquence.
Si vous êtes novice en matière d'API de cartes, le tutoriel Comment ajouter des cartes interactives à votre site web couvre les fondamentaux avant que celui-ci ne prenne le relais.
Structure des données de bien immobilier
Chaque annonce a besoin de coordonnées, d'un prix et de suffisamment de métadonnées pour la popup. Conservez-les sous forme de FeatureCollection GeoJSON, elles s'intègrent directement dans la source de carte sans transformation.
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"
}
}
]
};
Dans une vraie plateforme, ce tableau provient de votre API d'annonces, un appel fetch au chargement de la carte ou lors d'un changement de paramètre de requête.
Configuration de la carte
Initialisez la carte avec une vue centrée sur votre marché immobilier. L'API MapAtlas Maps est compatible avec Mapbox GL JS, donc l'appel d'initialisation est identique à ce que vous écririez pour Mapbox, avec une URL de tuile MapAtlas.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'map',
style: 'https://tiles.mapatlas.eu/styles/bright/style.json?key=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 13
});
Le style Bright fonctionne particulièrement bien pour les cartes immobilières : la base plus claire permet aux étiquettes de prix et aux marqueurs colorés de se démarquer sans encombrement visuel.
Regroupement avec étiquettes de prix
Le motif UI clé pour une carte immobilière est d'afficher des étiquettes de prix sur les marqueurs individuels et des bulles de comptage sur les cercles de cluster. L'option source cluster: true regroupe automatiquement les points proches. Vous ajoutez des couches séparées pour les clusters et les marqueurs individuels.
map.on('load', () => {
map.addSource('properties', {
type: 'geojson',
data: properties,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 60
});
// Cluster circles, size scales with listing count
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'
}
});
// Cluster count labels
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' }
});
// Individual property markers, white circle with price text
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
}
});
});
Interactions au clic
Cliquer sur un cluster zoome la carte pour révéler ses annonces individuelles. Cliquer sur un bien individuel ouvre une popup de détail.
// Zoom into clicked 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 });
}
);
});
// Detail popup for individual listing
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);
});
// Change cursor on 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 = ''; });
Filtre de plage de prix
Un curseur de plage qui filtre les annonces visibles est l'un des ajouts UI à plus haute valeur pour une carte immobilière. Le système d'expressions Mapbox GL JS vous permet de mettre à jour le filtre d'une source côté client sans aller-retour réseau ; le filtrage s'effectue dans le navigateur sur le GeoJSON déjà chargé.
function applyPriceFilter(min, max) {
const filter = ['all',
['!', ['has', 'point_count']],
['>=', ['get', 'price'], min],
['<=', ['get', 'price'], max]
];
map.setFilter('property-price', filter);
}
// Wire up range inputs
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);
Notez que la couche de cluster se met à jour automatiquement quand la source sous-jacente change : les biens filtrés hors de la couche de marqueurs individuels disparaissent également des comptages de clusters.
Recherche d'adresse avec l'API de géocodage
Permettez aux utilisateurs de saisir un quartier ou un code postal pour recentrer la carte. L'API de géocodage renvoie des features GeoJSON, donc les coordonnées s'insèrent directement dans map.flyTo.
async function searchArea(query) {
const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
url.searchParams.set('text', query);
url.searchParams.set('key', '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());
});
Pour la recherche par temps de trajet, "montrez-moi les biens à moins de 20 minutes de cette école", ajoutez une superposition d'isochrone avec l'API Travel Time de MapAtlas. L'article Cartes isochrones expliquées montre comment récupérer et afficher ce polygone.
Le composant React complet
En regroupant tout ce qui précède dans un composant React avec une gestion correcte du cycle de vie :
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://tiles.mapatlas.eu/styles/bright/style.json?key=${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
});
// Add cluster, cluster-label, property-price layers (see above)
// Add click handlers (see above)
});
return () => map.remove();
}, [apiKey]);
// Update filter when price range slider changes
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>
);
}
C'est le composant complet, en moins de 100 lignes avec le curseur de plage de prix et le nettoyage du cycle de vie.
Notes RGPD pour le PropTech UE
Quand un chercheur de bien saisit son adresse personnelle ou clique sur "utiliser ma position" sur votre carte, il partage des données personnelles au sens de l'article 4 du RGPD. Ces données de localisation révèlent où il vit, ce qui peut inférer d'autres attributs sensibles le concernant.
Étapes pratiques pour la conformité UE :
- Acheminez les requêtes de géocodage via votre backend plutôt que d'appeler l'API de géocodage directement depuis le navigateur. Votre backend peut supprimer les en-têtes identifiants avant de transmettre.
- Ne consignez pas ni ne conservez les coordonnées de recherche au-delà de la session sans base légale documentée et consentement spécifique collecté.
- Si vous implémentez "notifiez-moi quand un bien près de moi correspond à mes critères", traitez la zone de recherche stockée comme des données personnelles avec une période de conservation définie.
Le Guide RGPD du développeur UE pour les API de cartographie conformes couvre cela en détail, y compris les accords de sous-traitance et les exigences de résidence des données. MapAtlas traite toutes les données dans l'UE et fournit l'Accord de Traitement des Données requis par l'article 28 du RGPD.
La page de solution MapAtlas pour le secteur immobilier offre un contexte supplémentaire sur la façon dont les plateformes PropTech utilisent MapAtlas en production.
Prochaines étapes
- Créez une clé API MapAtlas gratuite et commencez avec le tier gratuit
- Ajoutez des superpositions de temps de trajet avec l'API Travel Time pour montrer aux acheteurs ce qui est accessible en 20 minutes
- Explorez le guide de stylisation de l'API Maps pour correspondre à l'identité visuelle de votre plateforme immobilière
Questions fréquentes
Puis-je utiliser MapAtlas pour un site d'annonces immobilières ?
Oui. MapAtlas est utilisé par des plateformes PropTech dans toute l'UE pour des cartes de recherche immobilière, des tableaux de bord d'investissement et des sites d'annonces de location. L'API Maps est compatible avec Mapbox GL JS, donc le code de carte immobilière existant basé sur Mapbox migre avec des modifications minimales.
Comment fonctionne le regroupement de marqueurs pour les cartes immobilières ?
Le regroupement rassemble les marqueurs de biens proches en un seul cercle affichant le comptage quand la carte est dézoomée. À mesure que l'utilisateur zoome, les clusters se divisent en marqueurs individuels. MapAtlas prend en charge nativement le regroupement de source GeoJSON : définissez cluster: true sur la source et le SDK gère le reste.
Les données de localisation des chercheurs de biens sont-elles soumises au RGPD ?
Oui. Quand un utilisateur effectue une recherche par son adresse personnelle ou sa position actuelle sur un site immobilier, cela constitue des données personnelles au sens de l'article 4 du RGPD. Les plateformes PropTech de l'UE devraient acheminer les requêtes de géocodage via un proxy backend et appliquer des politiques appropriées de minimisation et de rétention des données plutôt que de faire des appels API côté client qui enregistrent les positions des utilisateurs sur des serveurs tiers.

