في النهاية، تحتاج كل علامة تجارية للبيع بالتجزئة وامتياز وشركة خدمات إلى محدد موقع متجر. إنها الصفحة التي يزورها العملاء عندما يكونون بالفعل يريدون الشراء، يحتاجون فقط إلى معرفة أي فرع يجب أن يدخلوا إليه. الخطأ في هذه الصفحة يكلف تحويلات حقيقية. الحصول عليها بشكل صحيح أبسط مما يتوقعه معظم المطورين.
كان النهج القياسي هو خرائط Google، لكن يأتي مع هيكل تكاليف يعض على نطاق واسع. يمكن لموقع تجزئة مشغول يضم 50000 زائر شهري يحملون صفحة خرائط أن يجمعوا مئات اليورو في رسوم تحميل الخرائط والترميز الجغرافي بين عشية وضحاها. لا توجد طبقة مجانية لاستخدام الإنتاج بعد الآن، الفواتير غير واضحة، والموقف من GDPR للشركات الأوروبية التي تستخدم خدمة رسم خرائط قائمة على الولايات المتحدة يضيف عبء امتثال على القمة.
يبني هذا البرنامج التعليمي محدد موقع متجر كامل وخريطة تفاعلية وبحث العنوان ولوحة القائمة المتزامنة وتجميع العلامات والنوافذ المنبثقة عند النقر على التفاصيل، باستخدام MapAtlas Maps API و Geocoding API. لا يلزم أي إطار عمل. يناسب المثال العامل الكامل في حوالي 80 سطراً من HTML و JavaScript. يمكنك إسقاطه في كتلة HTML مخصصة في WordPress أو قسم Shopify أو أي نظام إدارة محتوى في نفس الفترة التي تقرأ فيها هذا.
بحلول النهاية سيكون لديك:
- خريطة متجهة معروضة تركز على شبكة المتاجر الخاصة بك
- علامات محملة من مصفوفة بيانات JSON مع التجميع عند التكبير المنخفض
- شريط بحث عنوان مدعوم بواسطة Geocoding API
- لوحة قائمة متزامنة تبرز عند النقر على الخريطة
- تخطيط عمودين سريع الاستجابة للهاتف المحمول
ما يحتاجه محدد الموقع فعلاً
قبل كتابة سطر واحد من الكود، يساعد أن تكون دقيقاً بشأن المتطلبات. محدد الموقع العامل له أربعة أجزاء متحركة:
- خريطة تعرض البلاطات وتقبل التنقل والتكبير وتعرض العلامات.
- مدخل بحث يقوم بتشفير عنوان الكاتب الذي يكتبه المستخدم بالإحداثيات، ثم يعيد توسيط الخريطة.
- طبقة علامة تحدد كل موقع متجر وتجميع المسامير القريبة في التكبير المنخفض وتفتح نافذة منبثقة بالتفاصيل عند النقر.
- لوحة قائمة التي تعرض المتاجر مرتبة حسب المسافة من الموقع الذي تم البحث عنه وتبرز المتجر النشط وتمرير المرات في مزامنة مع الخريطة.
هذا كل شيء. كل ميزة أخرى والاتجاهات وساعات العمل وأسهم المخزون هي تحسين يقع في الأعلى من هذه الأربعة. بناء النواة أولاً.
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;
}
}
Billing and GDPR Comparison with Google Maps
If you have been running Google Maps on a retail site and wondering why this month's bill came in higher than expected, you are not alone. The Maps JavaScript API charges per map load. The Places API charges per autocomplete session and per geocoding request. Those costs compound fast. A site doing 50,000 visits a month, each loading the store locator page once, spends around €140/month on map loads alone before a single geocoding call.
MapAtlas uses flat monthly plans. There is no per-load or per-request charge that spikes without warning. You can read the full breakdown in Google Maps API Pricing in 2026: The True Cost Breakdown and the MapAtlas vs. Google Maps comparison.
For EU developers the GDPR angle matters too. Google Maps routes data through US infrastructure. MapAtlas is EU-hosted, ISO 27001 certified, and processes all requests within the EU. For retail businesses that are already managing customer consent carefully, using an EU-native mapping provider removes one more third-party transfer from your privacy policy.
Putting It All Together
The complete store locator, HTML structure, CSS layout, map init, clustering, popup handling, list panel, search, and distance sort, fits comfortably in one file. The structure looks like this:
<!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>
The result is a production-ready store locator with zero external dependencies beyond the MapAtlas SDK. There is no build step, no framework, and no ongoing billing surprises.
If you need to add routing, "get directions from my location to this store", the Routing API takes the user's coordinates and the store's coordinates and returns a full turn-by-turn route you can draw on the map as a line layer. The How to Add Interactive Maps to Your Website tutorial covers that next step in detail.
Next Steps
- Sign up for a free MapAtlas API key, no credit card required
- Browse the Maps API documentation for clustering, custom styling, and layer options
- Explore the Geocoding API for postcode lookup, reverse geocoding, and address autocomplete
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.

