Каждый розничный бренд, франшиза и сервисный бизнес рано или поздно нуждается в поиске магазинов. Это страница, которую клиенты посещают, когда уже хотят купить и просто нуждаются в информации о ближайшем филиале. Ошибки на этой странице оборачиваются реальными потерями конверсии. Сделать её правильно проще, чем ожидают большинство разработчиков.
Стандартным подходом был Google Maps, но это сопряжено со структурой стоимости, которая больно бьёт при масштабировании. Загруженный розничный сайт с 50 000 ежемесячных посетителей, открывающих страницу с картой, может за ночь накопить сотни евро за загрузки карт и геокодирование. Бесплатного тарифа для производственного использования больше нет, выставление счетов непрозрачно, а ситуация с GDPR для европейских компаний, использующих картографический сервис на основе инфраструктуры США, добавляет сложностей с соответствием требованиям.
В этом руководстве создаётся полный поисковик магазинов с интерактивной картой, поиском по адресу, синхронизированной панелью списка, кластеризацией маркеров и всплывающими подсказками, используя Maps API MapAtlas и Geocoding API. Фреймворк не требуется. Полный рабочий пример умещается примерно в 80 строк HTML и JavaScript. Вы можете вставить его в кастомный HTML-блок WordPress, секцию Shopify или любую CMS в тот же день, когда прочитаете это.
К концу вы получите:
- Отрисованную векторную карту, центрированную на вашей сети магазинов
- Маркеры, загруженные из массива данных JSON с кластеризацией при малом масштабе
- Строку поиска адресов на основе Geocoding API
- Синхронизированную панель списка, которая подсвечивается при клике на карту
- Мобильный адаптивный двухколоночный макет
Что на самом деле нужно поисковику магазинов
Прежде чем писать код, полезно точно определить требования. Работающий поисковик магазинов состоит из четырёх частей:
- Карта, которая отображает тайлы, принимает перемещение и масштабирование, и показывает маркеры.
- Поисковая строка, которая геокодирует введённый пользователем адрес в координаты, затем перецентрирует карту.
- Слой маркеров, который отображает каждый магазин, кластеризует ближайшие пины при малом масштабе и открывает всплывающую подсказку по клику.
- Панель списка, которая показывает магазины, отсортированные по расстоянию от найденного местоположения, выделяет активный магазин и прокручивается синхронно с картой.
Это всё. Любая другая функция, маршруты, время работы, наличие товаров, является улучшением поверх этих четырёх. Сначала создайте основу.
Шаг 1: Загрузите SDK MapAtlas
Maps API MapAtlas совместим с интерфейсом Mapbox GL JS, поэтому любое руководство или плагин Mapbox GL JS работает напрямую. Добавьте ссылки CDN в <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>
Если вы используете npm:
npm install @mapmetrics/mapmetrics-gl
Получите бесплатный API-ключ на portal.mapmetrics.org/signup. Один ключ покрывает тайлы карт, геокодирование и маршрутизацию, не требуется управлять отдельными учётными данными.
Шаг 2: Определите данные магазинов
Данные магазинов представляют собой просто GeoJSON FeatureCollection. Каждая фича содержит координаты магазина и любые свойства, необходимые для всплывающей подсказки: название, адрес, телефон, время работы.
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"
}
}
]
};
В производственной среде вы бы получали эти данные из эндпоинта API или CMS. Структура остаётся той же, меняется только источник данных.
Шаг 3: Отобразите карту и добавьте кластеризацию
Инициализируйте карту, добавьте данные магазинов как GeoJSON-источник с включённой кластеризацией и создайте слои кругов кластеров и отдельных пинов. Кластеризация Mapbox GL JS встроена в определение источника, дополнительные плагины не требуются.
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], // Центр Нидерландов
zoom: 7
});
map.on('load', () => {
// Добавляем GeoJSON-источник с кластеризацией
map.addSource('stores', {
type: 'geojson',
data: stores,
cluster: true,
clusterMaxZoom: 12,
clusterRadius: 50
});
// Круги кластеров
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]
}
});
// Подписи с количеством в кластерах
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' }
});
// Отдельные пины магазинов
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'
}
});
});
Клик по кластеру увеличивает масштаб, открывая отдельные магазины. Клик по некластеризованному пину открывает всплывающую подсказку.
Шаг 4: Подключите всплывающие подсказки и панель списка
Когда пользователь нажимает на пин магазина, показывайте всплывающую подсказку на карте и выделяйте соответствующую карточку в панели списка. Оба взаимодействия должны быть двунаправленными: клик по карточке списка должен также переместить карту к этому магазину.
// Клик по некластеризованному магазину → открытие всплывающей подсказки + выделение карточки
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);
});
// Клик по кластеру → увеличение масштаба
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));
});
}
// Создание панели списка из данных магазинов
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);
});
}
Шаг 5: Добавьте поиск адресов с помощью Geocoding API
Строка поиска принимает введённое пользователем местоположение, геокодирует его через Geocoding API, перемещает карту к этой точке и пересортировывает панель списка по расстоянию.
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('Адрес не найден. Попробуйте город или почтовый индекс.');
return;
}
const [lng, lat] = data.features[0].geometry.coordinates;
// Перемещаем карту к найденному местоположению
map.flyTo({ center: [lng, lat], zoom: 10 });
// Сортируем список по расстоянию от найденной точки
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 => {
// Повторно отображаем каждую карточку (используем логику buildListPanel)
});
}
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);
});
Шаг 6: Адаптивный макет для мобильных устройств
Поисковик магазинов на мобильных должен располагаться вертикально: карта сверху, список снизу, а не рядом. Двадцать строк CSS решают это с одной точкой останова медиазапроса.
#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;
}
}
Сравнение выставления счетов и GDPR с Google Maps
Если вы запускали Google Maps на розничном сайте и задавались вопросом, почему счёт в этом месяце оказался выше ожидаемого, вы не одиноки. Maps JavaScript API взимает плату за каждую загрузку карты. Places API взимает плату за каждую сессию автодополнения и каждый запрос геокодирования. Эти расходы быстро накапливаются. Сайт с 50 000 посещений в месяц, каждое из которых включает загрузку страницы с поисковиком магазинов, тратит примерно 140 евро в месяц только на загрузки карт, не считая расходов на геокодирование.
MapAtlas использует фиксированные ежемесячные планы. Нет платы за загрузку или за запрос, которая резко вырастет без предупреждения. Полный разбор можно прочитать в статье про Google Maps API Pricing in 2026: The True Cost Breakdown и сравнении MapAtlas и Google Maps.
Для разработчиков из ЕС важен и аспект GDPR. Google Maps маршрутизирует данные через инфраструктуру США. MapAtlas размещён в ЕС, имеет сертификацию ISO 27001 и обрабатывает все запросы в ЕС. Для розничных компаний, которые уже тщательно управляют согласием клиентов, использование картографического провайдера, ориентированного на ЕС, убирает ещё одну передачу данных третьей стороне из вашей политики конфиденциальности.
Объединяем всё вместе
Полный поисковик магазинов: HTML-структура, CSS-макет, инициализация карты, кластеризация, обработка всплывающих подсказок, панель списка, поиск и сортировка по расстоянию, удобно умещается в одном файле. Структура выглядит так:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Поиск магазинов</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>
/* вставьте CSS из Шага 6 */
</style>
</head>
<body>
<div id="search-bar">
<input id="search-input" type="text" placeholder="Введите ваш город или почтовый индекс..." />
<button id="search-btn">Поиск</button>
</div>
<div id="locator-wrapper">
<div id="store-list"></div>
<div id="map"></div>
</div>
<script>
// вставьте данные магазинов, инициализацию карты, кластеризацию, всплывающие подсказки, панель списка и поиск из Шагов 2-5
</script>
</body>
</html>
Результат - готовый к производству поисковик магазинов без внешних зависимостей, кроме SDK MapAtlas. Нет этапа сборки, нет фреймворка, нет неожиданных счетов.
Если вам нужно добавить маршрутизацию, «проложить маршрут от моего местоположения до этого магазина», Routing API принимает координаты пользователя и координаты магазина и возвращает полный маршрут с пошаговыми инструкциями, который можно нарисовать на карте в виде линейного слоя. В руководстве How to Add Interactive Maps to Your Website подробно описан следующий шаг.
Следующие шаги
- Зарегистрируйтесь для получения бесплатного API-ключа MapAtlas, кредитная карта не требуется
- Изучите документацию Maps API по кластеризации, кастомным стилям и параметрам слоёв
- Ознакомьтесь с Geocoding API для поиска по почтовому индексу, обратного геокодирования и автодополнения адресов
Часто задаваемые вопросы
Можно ли создать поисковик магазинов без Google Maps?
Да. MapAtlas предоставляет совместимый с Mapbox GL JS Maps API и Geocoding API, которые покрывают все функции, необходимые поисковику магазинов: интерактивную карту, поиск по адресу, кластеризацию маркеров и всплывающие подсказки, без платы за загрузку и с полным соответствием требованиям GDPR.
Сколько стоит запуск поисковика магазинов на MapAtlas по сравнению с Google Maps?
MapAtlas примерно на 75% дешевле, чем Google Maps при аналогичном использовании. Google Maps взимает плату за каждую загрузку карты и каждый запрос геокодирования, что быстро накапливается на загруженном розничном сайте. MapAtlas использует фиксированные ежемесячные планы без неожиданных плат за запросы.
Работает ли MapAtlas на WordPress и Shopify?
Да. Поскольку MapAtlas является чистым JavaScript без зависимостей от фреймворка, вы можете встроить его в кастомный HTML-блок WordPress, секцию темы Shopify или любую CMS, которая позволяет добавить тег script и div.

