Mỗi thương hiệu bán lẻ, nhượp quyền và doanh nghiệp dịch vụ cuối cùng đều cần một công cụ tìm kiếm cửa hàng. Đó là trang mà khách hàng truy cập khi họ đã muốn mua, họ chỉ cần biết nên đi vào chi nhánh nào. Để trang đó sai có nghĩa là mất đi những chuyển đổi thực sự. Để làm đúng đơn giản hơn so với mong đợi của hầu hết các nhà phát triển.
Cách tiếp cận tiêu chuẩn là Google Maps, nhưng nó đi kèm với một cấu trúc chi phí gây ảnh hưởng ở quy mô. Một trang web bán lẻ bận rộn với 50.000 khách truy cập hàng tháng tải trang bản đồ có thể phải chịu hàng trăm euro phí tải bản đồ và địa mã hóa chỉ trong một đêm. Không còn tầng miễn phí cho sử dụng sản xuất nữa, lập hóa đơn là không rõ ràng, và tình huống GDPR cho các doanh nghiệp EU sử dụng dịch vụ lập bản đồ dựa trên Mỹ làm tăng thêm chi phí tuân thủ.
Hướng dẫn này xây dựng một công cụ tìm kiếm cửa hàng hoàn chỉnh, bản đồ tương tác, tìm kiếm địa chỉ, bảng danh sách được đồng bộ hóa, phân cụm điểm đánh dấu và bật lên chi tiết nhấp chuột, sử dụng API Bản Đồ MapAtlas và API Địa Mã Hóa. Không yêu cầu khung. Ví dụ làm việc đầy đủ phù hợp với khoảng 80 dòng HTML và JavaScript. Bạn có thể thả nó vào khối HTML tùy chỉnh WordPress, phần Shopify hoặc bất kỳ CMS nào cùng chiều sau khi bạn đọc điều này.
Vào cuối cùng bạn sẽ có:
- Một bản đồ vectơ được kết xuất tập trung vào mạng lưới cửa hàng của bạn
- Các điểm đánh dấu được tải từ mảng dữ liệu JSON với phân cụm ở mức zoom thấp
- Một thanh tìm kiếm địa chỉ được cung cấp bởi API Địa Mã Hóa
- Một bảng danh sách được đồng bộ hóa được làm nổi bật khi nhấp vào bản đồ
- Một bố cục hai cột thân thiện với thiết bị di động
Những Gì Một Công Cụ Tìm Kiếm Cửa Hàng Thực Sự Cần
Trước khi viết một dòng mã, sẽ hữu ích nếu chính xác về các yêu cầu. Một công cụ tìm kiếm cửa hàng hoạt động có bốn phần chuyển động:
- Một bản đồ kết xuất các ô, chấp nhận panning và zoom, và hiển thị các điểm đánh dấu.
- Một đầu vào tìm kiếm địa mã hóa địa chỉ được gõ của người dùng để tọa độ, sau đó định tâm lại bản đồ.
- Một lớp điểm đánh dấu vẽ sơ đồ mỗi vị trí cửa hàng, phân cụm các chân trước ở mức zoom thấp, và mở một bật lên chi tiết khi nhấp.
- Một bảng danh sách hiển thị các cửa hàng được sắp xếp theo khoảng cách từ vị trí được tìm kiếm, làm nổi bật cửa hàng hoạt động, và cuộn đồng bộ với bản đồ.
Đó là nó. Mọi tính năng khác, hướng dẫn, giờ mở cửa, tồn kho hàng, đều là sự nâng cao được xếp lớp trên bốn tính năng này. Xây dựng cốt lõi trước.
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;
}
}
So Sánh Lập Hóa Đơn Và GDPR Với Google Maps
Nếu bạn đã chạy Google Maps trên trang web bán lẻ và tự hỏi tại sao hóa đơn tháng này cao hơn kỳ vọng, bạn không đơn độc. Maps JavaScript API tính phí cho mỗi lần tải bản đồ. Places API tính phí cho mỗi phiên tự động hoàn thành và cho mỗi yêu cầu địa mã hóa. Những chi phí đó tổng hợp nhanh chóng. Một trang web thực hiện 50.000 lượt truy cập mỗi tháng, mỗi lần tải trang công cụ tìm kiếm cửa hàng một lần, chi tiêu khoảng €140/tháng chỉ cho tải bản đồ trước khi một lệnh gọi địa mã hóa duy nhất.
MapAtlas sử dụng các kế hoạch hàng tháng cố định. Không có phí cho mỗi lần tải hoặc cho mỗi yêu cầu tăng đột ngột mà không có cảnh báo. Bạn có thể đọc sự phân rã đầy đủ trong Định Giá Google Maps API Năm 2026: Sự Phân Rã Chi Phí Thực Tế và So Sánh MapAtlas Với Google Maps.
Đối với các nhà phát triển EU, góc nhìn GDPR cũng quan trọng. Google Maps định tuyến dữ liệu thông qua cơ sở hạ tầng Mỹ. MapAtlas được lưu trữ ở EU, được chứng nhận ISO 27001 và xử lý tất cả các yêu cầu trong EU. Đối với các doanh nghiệp bán lẻ đang cẩn thận quản lý sự đồng ý của khách hàng, sử dụng nhà cung cấp lập bản đồ gốc EU loại bỏ một lần chuyển tiếp của bên thứ ba khác khỏi chính sách quyền riêng tư của bạn.
Tập Hợp Tất Cả Lại Với Nhau
Công cụ tìm kiếm cửa hàng hoàn chỉnh, cấu trúc HTML, bố cục CSS, khởi tạo bản đồ, phân cụm, xử lý bật lên, bảng danh sách, tìm kiếm và sắp xếp khoảng cách, phù hợp thoải mái trong một tệp. Cấu trúc trông như thế này:
<!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>
Kết quả là một công cụ tìm kiếm cửa hàng sẵn sàng cho sản xuất mà không có bất kỳ phụ thuộc ngoài nào ngoài SDK MapAtlas. Không có bước xây dựng, không có khung, và không có những bất ngờ trong lập hóa đơn tiếp theo.
Nếu bạn cần thêm định tuyến, "lấy hướng dẫn từ vị trí của tôi đến cửa hàng này", API Định Tuyến lấy tọa độ của người dùng và tọa độ của cửa hàng và trả về toàn bộ tuyến đường rẽ cho rẽ mà bạn có thể vẽ trên bản đồ dưới dạng lớp đường. Hướng dẫn Cách Thêm Bản Đồ Tương Tác Vào Trang Web Của Bạn bao gồm bước tiếp theo đó chi tiết.
Các Bước Tiếp Theo
- Đăng ký khóa API MapAtlas miễn phí, không cần thẻ tín dụng
- Duyệt Tài Liệu API Bản Đồ để phân cụm, tạo kiểu tùy chỉnh và các tùy chọn lớp
- Khám phá API Địa Mã Hóa để tra cứu mã bưu chính, địa mã hóa ngược và tự động hoàn thành địa chỉ
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.

