A pesquisa de propriedade é um problema espacial. Compradores e inquilinos pensam em termos de bairros, tempos de deslocamento e proximidade de escolas, nao códigos postais e listas de ruas. Uma interface mapear-primeiro converte melhor do que uma interface lista-primeiro porque permite que os usuários ancorem sua pesquisa espacialmente antes de filtrar por preco, quartos ou qualquer outro atributo.
Este tutorial constrói um mapa de listagem de propriedade pronto para producao: marcadores agrupados em zoom baixo, rótulos de preco em marcadores individuais, clique para exibir em popup com detalhes de listagem, uma pesquisa de código postal que recentra o mapa e um controle deslizante de faixa de preco que filtra listagens visíveis em tempo real. O componente React completo tem menos de 100 linhas. A mesma lógica funciona em JavaScript vanilla se você preferir nenhuma estrutura.
Você também encontrará uma nota sobre obrigacoes de GDPR específicas para plataformas PropTech da UE, porque os dados de localizacao gerados pelas pesquisas de propriedade sao dados pessoais e precisam ser tratados adequadamente.
Se você é novo em APIs de mapa, o tutorial Como adicionar mapas interativos ao seu site cobre os fundamentos antes deste começar.
estrutura de dados de propriedade
Cada listagem precisa de coordenadas, um preco e metadados suficientes para o popup. Mantenha isto como um FeatureCollection GeoJSON, plugs diretamente na fonte do mapa sem transformacao.
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"
}
}
]
};
Em uma plataforma real, esse array vem de sua API de listagens, uma chamada fetch no carregamento do mapa ou uma mudanca de parametro de query.
configurando o mapa
Inicialize o mapa com uma visao central sobre seu mercado imobiliário. A API MapAtlas Maps é compatível com Mapbox GL JS, portanto a chamada de inicializacao é idêntica à que você escreveria para Mapbox, com uma URL de mosaico 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
});
O estilo Bright funciona particularmente bem para mapas de propriedade, a base mais clara permite que rótulos de preco e marcadores coloridos se destaquem sem desordem visual.
agrupamento com rótulos de preco
O padrão principal de IU para um mapa de propriedade é mostrar rótulos de preco em marcadores individuais e bolhas de contagem em círculos de cluster. A opcao de origem cluster: true agrupa pontos próximos automaticamente. Você adiciona camadas separadas para clusters e marcadores individuais.
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
}
});
});
interacoes de clique
Clicar em um cluster amplia o mapa para revelar suas listagens individuais. Clicar em uma propriedade individual abre um popup de detalhe.
// 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 = ''; });
filtro de faixa de preco
Um controle deslizante de intervalo que filtra listagens visíveis é uma das adicoes de IU de maior valor para um mapa de propriedade. O sistema de expressao Mapbox GL JS permite que você atualize o filtro de uma fonte do lado do cliente sem uma volta completa de rede, a filtragem acontece no navegador no GeoJSON já carregado.
function applyPriceFilter(min, max) {
const filter = ['all',
['!', ['has', 'point_count']],
['>=', ['get', 'price'], min],
['<=', ['get', 'price'], max]
];
map.setFilter('property-price', filter);
}
// conecte entradas de intervalo
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);
Note que a camada de cluster se atualiza automaticamente quando a fonte subjacente muda, as propriedades filtradas da camada de marcador individual também caem das contagens de cluster.
pesquisa de endereco com a API de geocodificacao
Deixe os usuários digitar um bairro ou código postal para recentrar o mapa. A API de geocodificacao retorna características GeoJSON, para que as coordenadas caiam diretamente em 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());
});
Para busca de tempo de viagem, "mostre-me propriedades dentro de 20 minutos desta escola", adicione uma sobreposicao isócrona usando a API de tempo de viagem MapAtlas. O artigo Mapas de isócronos explicados mostra como buscar e exibir esse polígono.
o componente React completo
Empacotando tudo acima em um componente React com gerenciamento de ciclo de vida apropriado:
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
});
// adicione camadas de cluster, cluster-label, property-price (veja acima)
// adicione manipuladores de clique (veja acima)
});
return () => map.remove();
}, [apiKey]);
// atualizar filtro quando o controle deslizante de faixa de preco muda
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>
);
}
Este é o componente completo, com menos de 100 linhas, incluindo o controle deslizante de faixa de preco e limpeza de ciclo de vida.
notas de GDPR para PropTech da UE
Quando um buscador de propriedade digita seu endereco de casa ou clica em "usar minha localizacao" em seu mapa, ele compartilha dados pessoais sob o artigo 4 do GDPR. Esses dados de localizacao revelam onde ele vive, o que pode inferir outros atributos sensíveis sobre ele.
Passos práticos para conformidade da UE:
- rotear solicitacoes de geocodificacao através de seu backend em vez de chamar a API de geocodificacao diretamente do navegador. Seu backend pode remover cabeçalhos de identificacao antes de encaminhar.
- nao registre ou persista coordenadas de pesquisa além da sessao, a menos que você tenha uma base legal documentada e tenha coletado consentimento específico.
- se você implementar "notifique-me quando uma propriedade perto de mim corresponder aos meus critérios", trate a area de pesquisa armazenada como dados pessoais com um período de retencao definido.
O Guia do desenvolvedor da UE para APIs de mapa em conformidade com GDPR cobre isto completamente, incluindo acordos de processador e requisitos de residência de dados. MapAtlas processa todos os dados dentro da UE e fornece o Acordo de processamento de dados necessário sob o artigo 28 do GDPR.
A página de solucao da indústria imobiliária tem contexto adicional sobre como as plataformas PropTech usam MapAtlas em producao.
próximas etapas
- inscreva-se para uma chave da API MapAtlas gratuita e comece com o nivel gratuito
- adicione sobreposicoes de tempo de viagem com a API de tempo de viagem, mostre aos compradores o que é alcancável em 20 minutos
- explore o guia de estilo da API de mapas para corresponder à identidade visual de sua plataforma de propriedade
Frequently Asked Questions
Can I use MapAtlas for a property listing website?
Yes. MapAtlas is used by PropTech platforms across the EU for property search maps, investment dashboards, and rental listing sites. The Maps API is Mapbox GL JS-compatible, so existing Mapbox-based property map code migrates with minimal changes.
How does marker clustering work for real estate maps?
Clustering groups nearby property markers into a single circle showing the count when the map is zoomed out. As the user zooms in, clusters split into individual markers. MapAtlas supports GeoJSON source clustering natively, set cluster: true on the source and the SDK handles the rest.
Is location data from property searchers subject to GDPR?
Yes. When a user searches by their home address or current location on a property site, that constitutes personal data under GDPR Article 4. EU PropTech platforms should route geocoding requests through a backend proxy and apply appropriate data minimisation and retention policies rather than making client-side API calls that log user locations to third-party servers.

