La ricerca di immobili è un problema spaziale. Chi compra o affitta casa ragiona per quartieri, tempi di spostamento e vicinanza alle scuole, non per CAP ed elenchi di vie. Un'interfaccia che parte dalla mappa rispecchia il modo in cui le persone cercano, perché permette all'utente di ancorare la ricerca a un'area prima di filtrare per prezzo, numero di camere o qualsiasi altro attributo.
Questo tutorial costruisce una mappa di annunci immobiliari pronta per la produzione: marker raggruppati in cluster ai livelli di zoom bassi, etichette di prezzo sui singoli marker, popup con i dettagli dell'annuncio al clic, una ricerca per CAP che ricentra la mappa e uno slider per la fascia di prezzo che filtra gli annunci visibili in tempo reale. Il componente React in sé è di circa 65 righe, e la versione completa con layer e gestori dei clic arriva a circa 150. La stessa logica funziona in JavaScript vanilla, se preferisci non usare un framework.
Troverai anche una nota sugli obblighi GDPR specifici per le piattaforme PropTech dell'UE, perché i dati di posizione generati dalle ricerche di immobili sono dati personali e vanno trattati di conseguenza.
Se sei alle prime armi con le API di mappe, il tutorial Come aggiungere mappe interattive al tuo sito web spiega le basi da cui parte questa guida.
Struttura dei dati degli immobili
Ogni annuncio ha bisogno di coordinate, di un prezzo e di metadati sufficienti per il popup. Mantieni i dati come FeatureCollection GeoJSON: si collegano direttamente alla source della mappa senza trasformazioni.
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"
}
}
]
};
In una piattaforma reale questo array arriva dalla tua API degli annunci, con una chiamata fetch al caricamento della mappa o al cambio di un parametro della query.
Come configurare la mappa
Inizializza la mappa con una vista centrata sul tuo mercato immobiliare. La Maps API di MapAtlas è compatibile con Mapbox GL JS, quindi la chiamata di inizializzazione è identica a quella che scriveresti per Mapbox, con un URL dei tile di 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
});
Uno stile chiaro e a basso contrasto funziona particolarmente bene per le mappe immobiliari: la base più chiara fa risaltare le etichette di prezzo e i marker colorati senza confusione visiva.
Clustering con etichette di prezzo
Il pattern di interfaccia chiave per una mappa immobiliare è mostrare le etichette di prezzo sui singoli marker e il numero di annunci sui cerchi dei cluster. L'opzione cluster: true della source raggruppa automaticamente i punti vicini. Poi aggiungi layer separati per i cluster e per i singoli marker.
map.on('load', () => {
map.addSource('properties', {
type: 'geojson',
data: properties,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 60
});
// Cerchi dei cluster, la dimensione cresce con il numero di annunci
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'
}
});
// Etichette con il conteggio del 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' }
});
// Marker dei singoli immobili, cerchio bianco con il prezzo
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
}
});
});
Interazioni al clic
Il clic su un cluster fa zoomare la mappa per mostrare i singoli annunci. Il clic su un singolo immobile apre un popup con i dettagli.
// Zoom sul cluster cliccato
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 con i dettagli del singolo annuncio
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);
});
// Cambia il cursore al passaggio del mouse
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 = ''; });
Filtro per fascia di prezzo
Uno slider che filtra gli annunci visibili è una delle aggiunte all'interfaccia che portano più valore a una mappa immobiliare. Il sistema di espressioni di Mapbox GL JS ti permette di aggiornare il filtro lato client senza round-trip di rete: il filtraggio avviene nel browser sul GeoJSON già caricato.
function applyPriceFilter(min, max) {
const filter = ['all',
['!', ['has', 'point_count']],
['>=', ['get', 'price'], min],
['<=', ['get', 'price'], max]
];
map.setFilter('property-price', filter);
}
// Collega gli input dello slider
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);
I cluster vengono calcolati dai dati della source, non dai filtri dei layer, quindi i conteggi dei cluster includono ancora gli annunci che lo slider ha nascosto. Per tenerli allineati, filtra gli annunci in JavaScript e passa il risultato a map.getSource('properties').setData().
Ricerca per indirizzo con la Geocoding API
Permetti agli utenti di digitare un quartiere o un CAP per ricentrare la mappa. La Geocoding API restituisce feature GeoJSON, quindi le coordinate si passano direttamente a map.flyTo.
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());
});
Per la ricerca per tempo di percorrenza, del tipo "mostrami gli immobili a 20 minuti da questa scuola", aggiungi una sovrapposizione isocrona con la Travel Time API di MapAtlas. L'articolo Mappe isocrone spiegate mostra come recuperare e visualizzare quel poligono.
Il componente React completo
Ecco tutto quanto visto sopra racchiuso in un componente React con una corretta gestione del ciclo di vita:
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
});
// Aggiungi i layer cluster, cluster-label e property-price (vedi sopra)
// Aggiungi i gestori dei clic (vedi sopra)
});
return () => map.remove();
}, [apiKey]);
// Aggiorna il filtro quando cambia lo slider della fascia di prezzo
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>
);
}
Questa è la struttura di base del componente, circa 65 righe compresi lo slider della fascia di prezzo e la pulizia del ciclo di vita; aggiungi il codice dei layer e dei gestori dei clic delle sezioni precedenti per completarlo.
Note sul GDPR per il PropTech europeo
Quando chi cerca casa digita il proprio indirizzo o clicca su "usa la mia posizione" sulla tua mappa, condivide dati personali ai sensi dell'Articolo 4, punto 1, del GDPR, che cita i dati relativi all'ubicazione come identificativo. Quei dati di posizione rivelano dove vive la persona e possono permettere di dedurre altre informazioni sensibili che la riguardano.
Passi pratici per la conformità nell'UE:
- Instrada le richieste di geocodifica attraverso il tuo backend invece di chiamare la Geocoding API direttamente dal browser. Il backend può rimuovere gli header identificativi prima di inoltrare la richiesta.
- Non registrare e non conservare le coordinate di ricerca oltre la sessione, a meno che tu non abbia una base giuridica documentata ai sensi dell'Articolo 6 del GDPR, come il consenso.
- Se implementi la funzione "avvisami quando un immobile vicino a me corrisponde ai miei criteri", tratta l'area di ricerca salvata come dato personale con un periodo di conservazione definito.
La guida per sviluppatori europei alle API di mappe conformi al GDPR tratta l'argomento in modo completo, inclusi gli accordi con i responsabili del trattamento e i requisiti di residenza dei dati. MapAtlas elabora i dati all'interno dell'UE e fornisce il Data Processing Agreement che l'Articolo 28, paragrafo 3, del GDPR richiede tra titolare e responsabile del trattamento.
La pagina della soluzione per il settore immobiliare offre altro contesto su come le piattaforme PropTech usano MapAtlas in produzione.
Prossimi passi
- Crea gratis la tua chiave API MapAtlas e parti dal piano gratuito
- Aggiungi sovrapposizioni dei tempi di percorrenza con la Travel Time API per mostrare agli acquirenti cosa è raggiungibile in 20 minuti
- Esplora la guida allo stile della Maps API per adattare la mappa all'identità visiva della tua piattaforma immobiliare
Domande frequenti
Posso usare MapAtlas per un sito di annunci immobiliari?
Sì. MapAtlas copre ciò che serve ai siti immobiliari: mappe di ricerca immobiliare, dashboard per investimenti e mappe di annunci di affitto. La Maps API è compatibile con Mapbox GL JS, quindi il codice esistente per mappe immobiliari basate su Mapbox si migra con modifiche minime.
Come funziona il clustering dei marker nelle mappe immobiliari?
Il clustering raggruppa i marker degli immobili vicini in un unico cerchio che mostra il conteggio quando la mappa è a zoom basso. Man mano che l'utente fa zoom, i cluster si dividono nei singoli marker. MapAtlas supporta nativamente il clustering delle source GeoJSON: imposta cluster: true sulla source e l'SDK si occupa del resto.
I dati di posizione di chi cerca casa sono soggetti al GDPR?
Sì. Quando un utente cerca a partire dal proprio indirizzo o dalla posizione attuale su un sito immobiliare, questi sono dati personali ai sensi dell'Articolo 4 del GDPR. Le piattaforme PropTech dell'UE dovrebbero instradare le richieste di geocodifica attraverso un proxy nel backend e applicare politiche adeguate di minimizzazione e conservazione dei dati, invece di effettuare chiamate API lato client che registrano la posizione degli utenti su server di terze parti.
Domande frequenti
Posso usare MapAtlas per un sito di annunci immobiliari?
Sì. MapAtlas copre ciò che serve ai siti immobiliari: mappe di ricerca immobiliare, dashboard per investimenti e mappe di annunci di affitto. La Maps API è compatibile con Mapbox GL JS, quindi il codice esistente per mappe immobiliari basate su Mapbox si migra con modifiche minime.
Come funziona il clustering dei marker nelle mappe immobiliari?
Il clustering raggruppa i marker degli immobili vicini in un unico cerchio che mostra il conteggio quando la mappa è a zoom basso. Man mano che l'utente fa zoom, i cluster si dividono nei singoli marker. MapAtlas supporta nativamente il clustering delle source GeoJSON: imposta `cluster: true` sulla source e l'SDK si occupa del resto.
I dati di posizione di chi cerca casa sono soggetti al GDPR?
Sì. Quando un utente cerca a partire dal proprio indirizzo o dalla posizione attuale su un sito immobiliare, questi sono dati personali ai sensi dell'Articolo 4 del GDPR. Le piattaforme PropTech dell'UE dovrebbero instradare le richieste di geocodifica attraverso un proxy nel backend e applicare politiche adeguate di minimizzazione e conservazione dei dati, invece di effettuare chiamate API lato client che registrano la posizione degli utenti su server di terze parti.

