Busca de imóveis é um problema espacial. Compradores e inquilinos pensam em bairros, tempo de deslocamento e proximidade de escolas, não em códigos postais e listas de ruas. Uma interface centrada no mapa combina com o jeito como as pessoas buscam, porque deixa o usuário ancorar a busca no espaço antes de filtrar por preço, número de quartos ou qualquer outro atributo.
Este tutorial constrói um mapa de anúncios de imóveis pronto para produção: marcadores agrupados em zoom baixo, rótulos de preço nos marcadores individuais, popup com os detalhes do anúncio ao clicar, uma busca por código postal que recentraliza o mapa e um slider de faixa de preço que filtra os anúncios visíveis em tempo real. O componente React em si tem cerca de 65 linhas, e a versão completa com camadas e handlers de clique chega a umas 150. A mesma lógica funciona em JavaScript puro, se você preferir não usar framework.
Você também vai encontrar uma nota sobre as obrigações do GDPR específicas para plataformas de PropTech na UE, porque os dados de localização gerados por buscas de imóveis são dados pessoais e precisam ser tratados como tal.
Se você está começando com APIs de mapas, o tutorial Como adicionar mapas interativos ao seu site cobre os fundamentos antes deste.
Estrutura dos dados dos imóveis
Cada anúncio precisa de coordenadas, um preço e metadados suficientes para o popup. Mantenha tudo como uma FeatureCollection GeoJSON: ela se conecta direto à source do mapa, sem nenhuma transformação.
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 da sua API de anúncios, por meio de uma chamada fetch no carregamento do mapa ou quando um parâmetro de query muda.
Configurar o mapa
Inicialize o mapa com uma visão centralizada no seu mercado imobiliário. A Maps API do MapAtlas é compatível com o Mapbox GL JS, então a chamada de inicialização é idêntica à que você escreveria para o Mapbox, trocando apenas a URL dos tiles pela do 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
});
Um estilo claro e de baixo contraste funciona muito bem para mapas de imóveis: a base mais clara deixa os rótulos de preço e os marcadores coloridos em destaque, sem poluição visual.
Agrupamento com rótulos de preço
O padrão de interface central de um mapa de imóveis é mostrar rótulos de preço nos marcadores individuais e bolhas com a contagem nos círculos dos clusters. A opção cluster: true da source agrupa pontos próximos automaticamente. Você adiciona camadas separadas para os clusters e para os marcadores individuais.
map.on('load', () => {
map.addSource('properties', {
type: 'geojson',
data: properties,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 60
});
// Círculos dos clusters, o tamanho acompanha o número de anúncios
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'
}
});
// Rótulos com a contagem de cada 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' }
});
// Marcadores individuais dos imóveis, círculo branco com o texto do preço
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
}
});
});
Interações de clique
Clicar em um cluster aproxima o mapa para mostrar os anúncios que ele contém. Clicar em um imóvel individual abre um popup com os detalhes.
// Aproxima o zoom no cluster clicado
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 de detalhes do anúncio individual
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);
});
// Muda o cursor ao passar o 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 por faixa de preço
Um slider de faixa que filtra os anúncios visíveis é uma das adições de interface mais valiosas em um mapa de imóveis. O sistema de expressões do Mapbox GL JS permite atualizar o filtro de uma camada no lado do cliente, sem ida e volta pela rede: a filtragem acontece no navegador, sobre o GeoJSON já carregado.
function applyPriceFilter(min, max) {
const filter = ['all',
['!', ['has', 'point_count']],
['>=', ['get', 'price'], min],
['<=', ['get', 'price'], max]
];
map.setFilter('property-price', filter);
}
// Conecta os inputs de faixa
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);
Observe que os clusters são calculados a partir dos dados da source, não dos filtros de camada, então as contagens dos clusters ainda incluem anúncios que o slider escondeu. Para manter tudo sincronizado, filtre os anúncios em JavaScript e passe o resultado para map.getSource('properties').setData().
Busca de endereço com a API de Geocodificação
Deixe o usuário digitar um bairro ou código postal para recentralizar o mapa. A API de Geocodificação retorna features GeoJSON, então as coordenadas vão direto para 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());
});
Para busca por tempo de viagem, como "mostre imóveis a até 20 minutos desta escola", adicione uma sobreposição de isócrona com a API de Tempo de Viagem do MapAtlas. O artigo Mapas de isócronas explicados mostra como buscar e exibir esse polígono.
O componente React completo
Juntando tudo o que foi mostrado acima em um componente React, com o gerenciamento correto do ciclo de vida:
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
});
// Adicione as camadas cluster, cluster-label e property-price (veja acima)
// Adicione os handlers de clique (veja acima)
});
return () => map.remove();
}, [apiKey]);
// Atualiza o filtro quando o slider de faixa de preço 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>
);
}
Essa é a estrutura base do componente, cerca de 65 linhas, incluindo o slider de faixa de preço e a limpeza do ciclo de vida; adicione o código de camadas e handlers de clique das seções anteriores para completá-lo.
Notas sobre GDPR para PropTech na UE
Quando alguém que procura um imóvel digita o endereço de casa ou clica em "usar minha localização" no seu mapa, está compartilhando dados pessoais nos termos do Artigo 4(1) do GDPR, que cita dados de localização como identificador. Esses dados de localização revelam onde a pessoa mora, o que pode permitir inferir outros atributos sensíveis sobre ela.
Passos práticos para a conformidade na UE:
- Encaminhe as requisições de geocodificação pelo seu backend em vez de chamar a API de Geocodificação direto do navegador. O backend pode remover cabeçalhos identificadores antes de repassar a requisição.
- Não registre nem armazene coordenadas de busca além da sessão, a menos que você tenha uma base legal documentada nos termos do Artigo 6 do GDPR, como o consentimento.
- Se você implementar um aviso do tipo "me avise quando um imóvel perto de mim atender aos meus critérios", trate a área de busca armazenada como dado pessoal, com um prazo de retenção definido.
O guia para desenvolvedores europeus sobre APIs de mapas em conformidade com o GDPR cobre o assunto por completo, incluindo contratos com operadores e requisitos de residência de dados. O MapAtlas processa os dados dentro da UE e fornece o Contrato de Processamento de Dados (DPA) que o Artigo 28(3) do GDPR exige entre controlador e operador.
A página da solução para o setor imobiliário traz mais contexto sobre como plataformas de PropTech usam o MapAtlas em produção.
Próximos passos
- Crie sua chave de API gratuita do MapAtlas e comece pelo plano gratuito
- Adicione sobreposições de tempo de viagem com a API de Tempo de Viagem e mostre aos compradores o que fica a até 20 minutos
- Explore o guia de estilização da Maps API para alinhar o mapa à identidade visual da sua plataforma de imóveis
Perguntas frequentes
Posso usar o MapAtlas em um site de anúncios de imóveis?
Sim. O MapAtlas cobre o que sites de imóveis precisam: mapas de busca de imóveis, dashboards de investimento e mapas de anúncios de aluguel. A Maps API é compatível com o Mapbox GL JS, então o código de mapas de imóveis já baseado em Mapbox migra com poucas mudanças.
Como funciona o agrupamento de marcadores em mapas de imóveis?
O clustering agrupa marcadores de imóveis próximos em um único círculo com a contagem quando o mapa está com zoom afastado. Conforme o usuário aproxima o zoom, os clusters se dividem em marcadores individuais. O MapAtlas suporta nativamente o clustering em sources GeoJSON: defina cluster: true na source e o SDK cuida do resto.
Os dados de localização de quem busca imóveis estão sujeitos ao GDPR?
Sim. Quando um usuário busca a partir do endereço de casa ou da localização atual em um site de imóveis, isso constitui dado pessoal segundo o Artigo 4 do GDPR. Plataformas de PropTech na UE devem encaminhar as requisições de geocodificação por um proxy no backend e aplicar políticas adequadas de minimização e retenção de dados, em vez de fazer chamadas de API no cliente que registram a localização dos usuários em servidores de terceiros.
Perguntas frequentes
Posso usar o MapAtlas em um site de anúncios de imóveis?
Sim. O MapAtlas cobre o que sites de imóveis precisam: mapas de busca de imóveis, dashboards de investimento e mapas de anúncios de aluguel. A Maps API é compatível com o Mapbox GL JS, então o código de mapas de imóveis já baseado em Mapbox migra com poucas mudanças.
Como funciona o agrupamento de marcadores em mapas de imóveis?
O clustering agrupa marcadores de imóveis próximos em um único círculo com a contagem quando o mapa está com zoom afastado. Conforme o usuário aproxima o zoom, os clusters se dividem em marcadores individuais. O MapAtlas suporta nativamente o clustering em sources GeoJSON: defina `cluster: true` na source e o SDK cuida do resto.
Os dados de localização de quem busca imóveis estão sujeitos ao GDPR?
Sim. Quando um usuário busca a partir do endereço de casa ou da localização atual em um site de imóveis, isso constitui dado pessoal segundo o Artigo 4 do GDPR. Plataformas de PropTech na UE devem encaminhar as requisições de geocodificação por um proxy no backend e aplicar políticas adequadas de minimização e retenção de dados, em vez de fazer chamadas de API no cliente que registram a localização dos usuários em servidores de terceiros.

