Toda marca minorista, franquicia y negocio de servicios eventualmente necesita un localizador de tiendas. Es la página que los clientes visitan cuando ya quieren comprar, solo necesitan saber qué sucursal visitar. Hacer esta página mal cuesta conversiones reales. Hacerlo bien es más sencillo de lo que la mayoría de desarrolladores esperan.
El enfoque estándar ha sido Google Maps, pero tiene una estructura de costos que duele a escala. Un sitio minorista ocupado con 50,000 visitantes mensuales cargando una página de mapas puede acumular cientos de euros en cargos por cargas de mapas y geocodificación durante la noche. Ya no hay nivel gratuito para uso en producción, la facturación es opaca, y la situación del RGPD para empresas europeas que usan un servicio de mapas basado en EE.UU. agrega sobrecarga de cumplimiento normativo.
Este tutorial construye un localizador de tiendas completo, mapa interactivo, búsqueda de direcciones, panel de lista sincronizado, agrupación de marcadores y ventanas emergentes de detalles al hacer clic, usando la API de Mapas MapAtlas y la API de Geocodificación. Sin marco requerido. El ejemplo de trabajo completo cabe en aproximadamente 80 líneas de HTML y JavaScript. Puedes insertarlo en un bloque HTML personalizado de WordPress, una sección de Shopify, o cualquier CMS la misma tarde que leas esto.
Al final tendrás:
- Un mapa vectorial representado centrado en tu red de tiendas
- Marcadores cargados desde una matriz de datos JSON con agrupación en zoom bajo
- Una barra de búsqueda de direcciones alimentada por la API de Geocodificación
- Un panel de lista sincronizado que se resalta al hacer clic en el mapa
- Un diseño de dos columnas receptivo para móviles
Lo Que Un Localizador de Tiendas Realmente Necesita
Antes de escribir una línea de código, ayuda ser preciso sobre los requisitos. Un localizador de tiendas funcional tiene cuatro partes móviles:
- Un mapa que representa tiles, acepta panoramización y zoom, y muestra marcadores.
- Una entrada de búsqueda que geocodifica la dirección escrita por el usuario en coordenadas, luego recentra el mapa.
- Una capa de marcadores que traza cada ubicación de tienda, agrupa pines cercanos en zoom bajo, y abre una ventana emergente de detalles al hacer clic.
- Un panel de lista que muestra tiendas ordenadas por distancia desde la ubicación buscada, resalta la tienda activa, y se desplaza sincronizado con el mapa.
Eso es. Todas las otras características, direcciones, horarios de apertura, stock de inventario, son mejoras superpuestas en estos cuatro. Construye el núcleo primero.
Step 1: Load the MapAtlas SDK
The MapAtlas Maps API is compatible with the Mapbox GL JS interface, so any Mapbox GL JS tutorial or plugin works directly. Add the CDN links to your page <head>:
<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>
If you are using npm:
npm install @mapmetrics/mapmetrics-gl
Get your free API key at portal.mapmetrics.org/signup. One key covers map tiles, geocoding, and routing, no separate credentials to juggle.
Step 2: Define Your Store Data
Store data is just a GeoJSON FeatureCollection. Each feature carries the store's coordinates and whatever properties your popup needs: name, address, phone, opening hours.
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"
}
}
]
};
In production you would fetch this from an API endpoint or a CMS. The structure stays the same, the only difference is where the data originates.
Step 3: Render the Map and Add Clustering
Initialize the map, add the store data as a GeoJSON source with clustering enabled, and paint the cluster circles and individual pin layer. Mapbox GL JS clustering is built into the source definition, no plugin required.
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'
}
});
});
Clicking a cluster zooms in to reveal individual stores. Clicking an unclustered pin opens a popup.
Step 4: Wire Up Popups and the List Panel
When a user clicks a store pin, show a popup on the map and highlight the matching card in the list panel. Both interactions should be bidirectional, clicking a list card should also fly the map to that store.
// 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);
});
}
Step 5: Add Address Search with the Geocoding API
The search bar takes a user's typed location, geocodes it via the Geocoding API, flies the map to that point, and re-sorts the list panel by distance.
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);
});
Step 6: Mobile-Responsive Layout
A store locator on mobile must stack vertically, map on top, list below, rather than side by side. Twenty lines of CSS handles this with a single media query breakpoint.
#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;
}
}
Comparación de Facturación y RGPD con Google Maps
Si has estado ejecutando Google Maps en un sitio minorista y te has preguntado por qué la factura de este mes fue más alta de lo esperado, no estás solo. La API de JavaScript de Mapas cobra por carga de mapa. La API de Places cobra por sesión de autocompletado y por solicitud de geocodificación. Esos costos se componen rápidamente. Un sitio con 50,000 visitas al mes, cada uno cargando la página del localizador de tiendas una vez, gasta alrededor de €140/mes solo en cargas de mapas antes de una sola llamada de geocodificación.
MapAtlas utiliza planes mensuales fijos. No hay carga por carga o por solicitud que aumente sin advertencia. Puedes leer el desglose completo en Precios de la API de Google Maps en 2026: El Desglose Real de Costos y la comparación de MapAtlas vs. Google Maps.
Para desarrolladores europeos, el ángulo del RGPD también importa. Google Maps enruta datos a través de infraestructura de EE.UU. MapAtlas se aloja en la UE, está certificado ISO 27001 y procesa todas las solicitudes dentro de la UE. Para negocios minoristas que ya están gestionando cuidadosamente el consentimiento del cliente, usar un proveedor de mapas nativo de la UE elimina una transferencia de terceros más de su política de privacidad.
Juntándolo Todo
El localizador de tiendas completo, estructura HTML, diseño CSS, inicialización de mapas, agrupación, manejo de ventanas emergentes, panel de lista, búsqueda y ordenamiento de distancia, cabe cómodamente en un archivo. La estructura se ve así:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Store Locator</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>
/* paste the CSS from Step 6 here */
</style>
</head>
<body>
<div id="search-bar">
<input id="search-input" type="text" placeholder="Enter your city or postcode…" />
<button id="search-btn">Search</button>
</div>
<div id="locator-wrapper">
<div id="store-list"></div>
<div id="map"></div>
</div>
<script>
// paste store data, map init, clustering, popup, list panel, and search from Steps 2–5
</script>
</body>
</html>
El resultado es un localizador de tiendas listo para producción sin dependencias externas más allá del SDK de MapAtlas. No hay paso de construcción, no hay marco, y sin sorpresas de facturación continua.
Si necesitas agregar enrutamiento, "obtener direcciones desde mi ubicación a esta tienda", la API de Enrutamiento toma las coordenadas del usuario y las coordenadas de la tienda y devuelve una ruta completa paso a paso que puedes dibujar en el mapa como una capa de línea. El tutorial Cómo Agregar Mapas Interactivos a Tu Sitio Web cubre ese próximo paso en detalle.
Próximos Pasos
- Regístrate para una clave API gratuita de MapAtlas, sin tarjeta de crédito requerida
- Examina la documentación de la API de Mapas para opciones de agrupación, estilos personalizados y capas
- Explora la API de Geocodificación para búsqueda de códigos postales, geocodificación inversa y autocompletado de direcciones
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.

