Toda marca de varejo, franquia e negócio de serviço eventualmente precisa de um localizador de lojas. É a página que os clientes visitam quando já querem comprar, eles apenas precisam saber qual filial entrar. Errar essa página custa conversões reais. Acertar é mais simples do que a maioria dos desenvolvedores espera.
A abordagem padrão tem sido o Google Maps, mas isso tem uma estrutura de custos que morde em escala. Um site varejista ocupado com 50.000 visitantes mensais carregando uma página de mapas pode acumular centenas de euros em cargas de mapa e encargos de geocodificação durante a noite. Não há mais nível gratuito para uso em produção, a cobrança é opaca e a situação de GDPR para empresas da UE usando um serviço de mapeamento sediado nos EUA adiciona sobrecarga de conformidade.
Este tutorial constrói um localizador de lojas completo, mapa interativo, pesquisa de endereço, painel de lista sincronizado, clustering de marcadores e popups de clique para detalhe, usando a API de Mapas MapAtlas e API de Geocodificação. Nenhuma estrutura necessária. O exemplo funcional completo cabe em aproximadamente 80 linhas de HTML e JavaScript. Você pode deixá-lo em um bloco de HTML personalizado do WordPress, uma seção do Shopify ou qualquer CMS na mesma tarde que ler isto.
No final você terá:
- Um mapa de vetor renderizado centrado em sua rede de lojas
- Marcadores carregados de um array de dados JSON com clustering em zoom baixo
- Uma barra de pesquisa de endereço alimentada pela API de Geocodificação
- Um painel de lista sincronizado que destaca no clique do mapa
- Um layout responsivo de duas colunas para celular
O Que Um Localizador de Lojas Realmente Precisa
Antes de escrever uma linha de código, é útil ser preciso sobre os requisitos. Um localizador de lojas funcional tem quatro partes móveis:
- Um mapa que renderiza tiles, aceita panorâmica e zoom, e mostra marcadores.
- Uma entrada de pesquisa que geocodifica o endereço digitado do usuário para coordenadas, depois recentra o mapa.
- Uma camada de marcador que plota cada localização de loja, agrupa pinos próximos em zoom baixo e abre um popup de detalhe no clique.
- Um painel de lista que mostra lojas classificadas por distância do local pesquisado, destaca a loja ativa e rola sincronizado com o mapa.
É isso. Toda outra feature, direções, horários de funcionamento, estoque de inventário, é um aprimoramento sobreposto nesses quatro. Construa o núcleo primeiro.
Passo 1: Carregue o SDK MapAtlas
A API de Mapas MapAtlas é compatível com a interface Mapbox GL JS, portanto qualquer tutorial ou plugin Mapbox GL JS funciona diretamente. Adicione os links CDN ao seu <head> da página:
<link
rel="stylesheet"
href="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css"
/>
<script src="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.js"></script>
Se você estiver usando npm:
npm install @mapmetrics/mapmetrics-gl
Obtenha sua chave API gratuita em portal.mapmetrics.org/signup. Uma chave cobre tiles de mapa, geocodificação e roteamento, sem credenciais separadas para se preocupar.
Passo 2: Defina Seus Dados de Loja
Os dados da loja são apenas uma FeatureCollection GeoJSON. Cada feature carrega as coordenadas da loja e qualquer propriedade que seu popup precise: nome, endereço, telefone, horários de funcionamento.
const stores = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9041, 52.3676] },
properties: {
id: 1,
name: "Amsterdam Central",
address: "Stationsplein 12, 1012 AB Amsterdam",
phone: "+31 20 123 4567",
hours: "Mon–Sat 09:00–20:00"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.4777, 51.9244] },
properties: {
id: 2,
name: "Rotterdam Lijnbaan",
address: "Lijnbaan 10, 3012 EL Rotterdam",
phone: "+31 10 987 6543",
hours: "Mon–Sat 09:00–21:00"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [5.1214, 52.0907] },
properties: {
id: 3,
name: "Utrecht Centrum",
address: "Oudegracht 45, 3511 AB Utrecht",
phone: "+31 30 555 1234",
hours: "Mon–Sun 10:00–19:00"
}
}
]
};
Em produção você buscaria isso de um endpoint de API ou CMS. A estrutura permanece a mesma, a única diferença é de onde os dados originam.
Passo 3: Renderize o Mapa e Adicione Clustering
Inicialize o mapa, adicione os dados da loja como uma fonte GeoJSON com clustering habilitado e processe os círculos de cluster e camada de pino individual. O clustering Mapbox GL JS é incorporado na definição de fonte, nenhum plugin necessário.
const map = new mapmetricsgl.Map({
container: 'map',
style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
center: [5.2913, 52.1326], // Centre of the Netherlands
zoom: 7
});
map.on('load', () => {
// Add GeoJSON source with clustering
map.addSource('stores', {
type: 'geojson',
data: stores,
cluster: true,
clusterMaxZoom: 12,
clusterRadius: 50
});
// Cluster circles
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'stores',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#3B82F6',
'circle-radius': ['step', ['get', 'point_count'], 20, 10, 28, 30, 36]
}
});
// Cluster count labels
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'stores',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-size': 13
},
paint: { 'text-color': '#ffffff' }
});
// Individual store pins
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'stores',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#EF4444',
'circle-radius': 8,
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
});
});
Clicar em um cluster faz zoom para revelar lojas individuais. Clicar em um pino não agrupado abre um popup.
Passo 4: Conecte Popups e o Painel de Lista
Quando um usuário clica em um pino de loja, mostre um popup no mapa e destaque o card correspondente no painel de lista. Ambas as interações devem ser bidirecionais, clicar em um card de lista também deve voar o mapa para aquela loja.
// Click unclustered store → open popup + highlight list card
map.on('click', 'unclustered-point', (e) => {
const { coordinates } = e.features[0].geometry;
const { name, address, phone, hours, id } = e.features[0].properties;
new mapmetricsgl.Popup()
.setLngLat(coordinates)
.setHTML(`
<strong>${name}</strong>
<p>${address}</p>
<p>${phone}</p>
<p>${hours}</p>
`)
.addTo(map);
highlightCard(id);
});
// Click cluster → zoom in
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
const clusterId = features[0].properties.cluster_id;
map.getSource('stores').getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) return;
map.easeTo({ center: features[0].geometry.coordinates, zoom });
});
});
function highlightCard(id) {
document.querySelectorAll('.store-card').forEach(card => {
card.classList.toggle('active', card.dataset.id === String(id));
});
}
// Build list panel from store data
function buildListPanel() {
const list = document.getElementById('store-list');
stores.features.forEach(({ properties, geometry }) => {
const card = document.createElement('div');
card.className = 'store-card';
card.dataset.id = properties.id;
card.innerHTML = `
<strong>${properties.name}</strong>
<p>${properties.address}</p>
<small>${properties.hours}</small>
`;
card.addEventListener('click', () => {
map.flyTo({ center: geometry.coordinates, zoom: 14 });
highlightCard(properties.id);
});
list.appendChild(card);
});
}
Passo 5: Adicione Pesquisa de Endereço com a API de Geocodificação
A barra de pesquisa pega o local digitado de um usuário, geocodifica-o via API de Geocodificação, voa o mapa até esse ponto e reordena o painel de lista por distância.
async function searchLocation(query) {
const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
url.searchParams.set('text', query);
url.searchParams.set('key', 'YOUR_API_KEY');
const res = await fetch(url);
const data = await res.json();
if (!data.features.length) {
alert('Address not found. Try a city or postcode.');
return;
}
const [lng, lat] = data.features[0].geometry.coordinates;
// Fly map to searched location
map.flyTo({ center: [lng, lat], zoom: 10 });
// Sort list by distance from searched point
const sorted = [...stores.features].sort((a, b) => {
const distA = haversine(lat, lng, a.geometry.coordinates[1], a.geometry.coordinates[0]);
const distB = haversine(lat, lng, b.geometry.coordinates[1], b.geometry.coordinates[0]);
return distA - distB;
});
document.getElementById('store-list').innerHTML = '';
sorted.forEach(feature => {
// Re-render each card (reuse buildListPanel logic)
});
}
function haversine(lat1, lon1, lat2, lon2) {
const R = 6371;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
document.getElementById('search-btn').addEventListener('click', () => {
const query = document.getElementById('search-input').value.trim();
if (query) searchLocation(query);
});
Passo 6: Layout Responsivo para Celular
Um localizador de lojas em celular deve empilhar verticalmente, mapa no topo, lista abaixo, em vez de lado a lado. Vinte linhas de CSS lidam com isso com um único ponto de quebra de consulta de mídia.
#locator-wrapper {
display: flex;
height: 600px;
gap: 0;
}
#store-list {
width: 300px;
overflow-y: auto;
border-right: 1px solid #e5e7eb;
padding: 12px;
}
#map {
flex: 1;
}
.store-card {
padding: 12px;
border-radius: 8px;
cursor: pointer;
margin-bottom: 8px;
border: 2px solid transparent;
transition: border-color 0.2s;
}
.store-card.active {
border-color: #3B82F6;
background: #EFF6FF;
}
@media (max-width: 640px) {
#locator-wrapper {
flex-direction: column;
height: auto;
}
#store-list {
width: 100%;
border-right: none;
border-top: 1px solid #e5e7eb;
height: 280px;
}
#map {
height: 350px;
}
}
Comparação de Faturamento e GDPR com Google Maps
Se você tem mantido o Google Maps em um site varejista e se perguntando por que a fatura deste mês chegou mais alta que o esperado, você não está sozinho. A API de Mapas JavaScript cobra por carregamento de mapa. A API de Places cobra por sessão de preenchimento automático e por solicitação de geocodificação. Esses custos se multiplicam rapidamente. Um site fazendo 50.000 visitas por mês, cada uma carregando a página do localizador de lojas uma vez, gasta cerca de €140/mês apenas em carregamentos de mapa antes de uma única chamada de geocodificação.
MapAtlas usa planos mensais fixos. Não há cobrança por carregamento ou por solicitação que aumente sem aviso. Você pode ler o detalhamento completo em Preços da API do Google Maps em 2026: Divisão do Custo Real e a comparação MapAtlas vs. Google Maps.
Para desenvolvedores da UE, o ângulo GDPR também importa. O Google Maps roteia dados através da infraestrutura dos EUA. MapAtlas é hospedado na UE, certificado ISO 27001 e processa todas as solicitações dentro da UE. Para empresas varejistas que já gerenciam cuidadosamente o consentimento do cliente, usar um provedor de mapeamento nativo da UE remove mais uma transferência de terceiros de sua política de privacidade.
Reunindo Tudo
O localizador de lojas completo, estrutura HTML, layout CSS, inicialização de mapa, clustering, manipulação de popup, painel de lista, pesquisa e ordenação de distância, cabe confortavelmente em um arquivo. A estrutura parece assim:
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Localizador de Lojas</title>
<link rel="stylesheet"
href="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css" />
<script src="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.js"></script>
<style>
/* cole o CSS do Passo 6 aqui */
</style>
</head>
<body>
<div id="search-bar">
<input id="search-input" type="text" placeholder="Digite sua cidade ou código postal…" />
<button id="search-btn">Pesquisar</button>
</div>
<div id="locator-wrapper">
<div id="store-list"></div>
<div id="map"></div>
</div>
<script>
// cole dados de loja, init de mapa, clustering, popup, painel de lista e pesquisa dos Passos 2-5
</script>
</body>
</html>
O resultado é um localizador de lojas pronto para produção com zero dependências externas além do SDK MapAtlas. Não há etapa de construção, nenhuma estrutura e nenhuma surpresa de cobrança contínua.
Se você precisar adicionar roteamento, "obter direções do meu local para esta loja", a API de Roteamento pega as coordenadas do usuário e as coordenadas da loja e retorna uma rota completa passo a passo que você pode desenhar no mapa como uma camada de linha. O tutorial Como Adicionar Mapas Interativos ao Seu Website cobre esse próximo passo em detalhes.
Próximos Passos
- Inscreva-se para uma chave API MapAtlas gratuita, nenhum cartão de crédito necessário
- Navegue pela documentação da API de Mapas para clustering, estilo personalizado e opções de camada
- Explore a API de Geocodificação para pesquisa de código postal, geocodificação reversa e autocomplete de endereço
Frequently Asked Questions
Can I build a store locator without Google Maps?
Yes. MapAtlas provides a Mapbox GL JS-compatible Maps API and a Geocoding API that cover every feature a store locator needs, interactive map, address search, marker clustering, and popups, with no per-load billing and full GDPR compliance.
How much does a store locator cost to run on MapAtlas vs Google Maps?
MapAtlas is roughly 75% cheaper than Google Maps for equivalent usage. Google Maps charges per map load and per geocoding request, which adds up fast on a busy retail site. MapAtlas uses flat monthly plans with no per-request surprises.
Does MapAtlas work on WordPress and Shopify?
Yes. Because MapAtlas is pure JavaScript with no framework dependency, you can embed it in a WordPress custom HTML block, a Shopify theme section, or any CMS that lets you add a script tag and a div.

