Разработка системы выбора пункта выдачи на карте для интернет-магазина
Покупатель открывает шаг доставки, видит список из 47 адресов пунктов выдачи, пытается найти нужный район в выпадающем списке и закрывает вкладку. Наш инженерный опыт показывает: без интерактивной карты до 40% пользователей бросают заказ на этом этапе. Мы — команда с 7-летним опытом в разработке e-commerce решений. С нашим решением человек сразу видит точки на карте рядом с домом, работой, по дороге — и выбирает за 5 секунд. Мы внедрили такие карты для 30+ интернет-магазинов, средний рост конверсии на этапе выбора доставки — 18%.
Проблемы выбора ПВЗ и как карта их решает
Google Analytics показывает, что страница выбора доставки — одна из самых «проседающих»: до 40% пользователей уходят, не выбрав способ. Без карты покупатель вынужден пролистывать длинный список адресов, сопоставлять с мысленной картой города и часто ошибается или бросает оформление. Типичные сложности:
- Длинные списки без визуальной привязки — названия улиц ни о чём не говорят, если человек плохо знает район.
- Отсутствие информации о загруженности — ПВЗ может быть переполнен, но в списке этого не видно.
- Невозможность сравнить несколько точек одновременно — приходится запоминать адреса и переключаться между вкладками.
Карта возвращает наглядность: маркеры показывают расположение, всплывающие подсказки — детали (режим работы, сроки, стоимость), группировка в кластеры — масштабируемость. В результате меньше брошенных корзин и экономия бюджета на ретаргетинге. Экономия на логистике достигает 20-30% за счёт снижения возвратов.
Источники данных о ПВЗ
Пункты выдачи приходят из API служб доставки. У каждой своя структура, но суть одна — список объектов с координатами, адресом, часами работы, ограничениями по весу и габаритам.
СДЭК:
GET https://api.cdek.ru/v2/deliverypoints?city_code=44&weight_max=30&type=PVZ
Authorization: Bearer {token}
Boxberry:
GET https://api.boxberry.ru/json.php?token={token}&method=ListPoints&CityCode=77&prepaid=1
Как мы кешируем справочник ПВЗ?
Список ПВЗ меняется редко — раз в сутки. Запрашивать его при каждом открытии страницы — расточительство. Мы синхронизируем данные по расписанию: каждую ночь artisan-команда загружает все точки от каждого перевозчика, очищает таблицу и вставляет актуальный набор. Пользователь получает данные из локальной БД за 10–20 мс вместо 500–2000 мс от API.
Пример artisan-команды для синхронизации
class SyncPickupPoints extends Command
{
public function handle(CdekService $cdek, BoxberryService $boxberry): void
{
$carriers = [
'cdek' => fn() => $cdek->getAllPickupPoints(),
'boxberry' => fn() => $boxberry->getAllPickupPoints(),
];
foreach ($carriers as $carrier => $fetcher) {
$points = $fetcher();
$this->info("$carrier: {$points->count()} points");
PickupPoint::where('carrier', $carrier)->delete();
PickupPoint::insert(
$points->map(fn($p) => [
'carrier' => $carrier,
'external_id' => $p['code'],
'name' => $p['name'],
'address' => $p['address'],
'city' => $p['city'],
'lat' => $p['lat'],
'lng' => $p['lng'],
'work_time' => $p['work_time'],
'max_weight' => $p['max_weight_kg'],
'cash_allowed' => $p['cash_allowed'],
'updated_at' => now(),
])->toArray()
);
}
$this->info('Done');
}
}
Как мы оптимизируем геопоиск?
Для поиска ближайших ПВЗ используем PostGIS. Сравните два подхода: PostGIS с пространственным индексом GIST против формулы гаверсинуса на стороне SQL. Выигрыш в скорости — 10–30 раз на наборах более 10 000 точек.
| Критерий | PostGIS (GIST) | Формула гаверсинуса в SQL |
|---|---|---|
| Время выполнения запроса (10 км, 20 точек) | 5–15 мс | 50–200 мс |
| Точность | Расстояние по сфероиду | Приблизительное |
| Поддержка радиуса | Встроенная (ST_DWithin) | Расчёт в WHERE |
| Индексирование | Пространственный индекс GIST | Нет, полное сканирование |
Мы выбираем PostGIS: он даёт выигрыш в скорости в 10–30 раз на больших наборах данных. Документация PostGIS утверждает, что пространственные индексы делают запросы к геоданным на порядок быстрее.
CREATE EXTENSION IF NOT EXISTS postgis;
ALTER TABLE pickup_points ADD COLUMN location geography(POINT, 4326);
UPDATE pickup_points SET location = ST_SetSRID(ST_MakePoint(lng, lat), 4326);
CREATE INDEX idx_pickup_points_location ON pickup_points USING GIST(location);
SELECT
id, carrier, name, address, work_time, cash_allowed,
ST_Distance(location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)) AS distance_m
FROM pickup_points
WHERE
max_weight >= :weight
AND ST_DWithin(location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography, 10000)
ORDER BY distance_m
LIMIT 20;
Рендеринг карты с кластеризацией
Отметим: когда точек много — несколько тысяч — рендерить каждую как отдельный DOM-элемент нельзя, браузер зависнет. Используем Leaflet + MarkerClusterGroup. Загрузка данных происходит только для видимой области карты (событие moveend).
import L from 'leaflet';
import 'leaflet.markercluster';
const map = L.map('pickup-map').setView([55.7558, 37.6173], 11);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
const markers = L.markerClusterGroup({
maxClusterRadius: 50,
iconCreateFunction: (cluster) => {
const count = cluster.getChildCount();
return L.divIcon({
html: `<div class="cluster-icon">${count}</div>`,
className: '',
iconSize: [40, 40],
});
},
});
map.on('moveend', async () => {
const bounds = map.getBounds();
const response = await fetch('/api/pickup-points?' + new URLSearchParams({
north: bounds.getNorth(),
south: bounds.getSouth(),
east: bounds.getEast(),
west: bounds.getWest(),
weight: cartWeight,
}));
const points = await response.json();
markers.clearLayers();
points.forEach((point) => {
const marker = L.marker([point.lat, point.lng], {
icon: carrierIcon(point.carrier),
});
marker.bindPopup(buildPopup(point));
marker.on('click', () => selectPickupPoint(point));
markers.addLayer(marker);
});
});
map.addLayer(markers);
Что входит в работу
При заказе разработки вы получаете:
- Интеграцию с 1–5 службами доставки (API, синхронизация, кеширование)
- Геопоиск через PostGIS или альтернативный алгоритм
- Карту с кластеризацией, фильтрами и попапами
- Адаптивную вёрстку под мобильные устройства
- Определение города покупателя (геолокация + IP)
- Документацию по API и инструкцию по обновлению справочника
- 2 недели бесплатной поддержки после запуска
Процесс работы
- Аналитика — изучаем вашу форму заказа, список служб доставки, типичные сценарии покупателей.
- Проектирование — согласовываем структуру данных, API-методы, логику фильтров.
- Реализация — пишем код, настраиваем cron-задачи, подключаем карту.
- Тестирование — проверяем на реальных данных, имитируем 1000+ точек.
- Деплой и обучение — выгружаем на боевой сервер, передаём доступы и документацию.
Сроки разработки
| Этап | Срок |
|---|---|
| Одна служба доставки, карта с кластеризацией | 4–6 дней |
| Агрегатор 3–5 служб, фильтры, геолокация | 2 недели |
| Добавление карты в существующую форму заказа | 3–5 дней |
Окончательная стоимость рассчитывается индивидуально — зависит от числа служб, сложности фильтров и необходимости переработки формы заказа. Грамотная карта ПВЗ окупается за счёт роста конверсии — вложения возвращаются в течение 3–6 месяцев. Средний срок окупаемости карты ПВЗ — 4 месяца за счёт роста конверсии.
Свяжитесь с нами, чтобы обсудить ваш проект и получить демо работающей карты. Закажите карту ПВЗ для вашего интернет-магазина — мы улучшим конверсию доставки.
Подробнее о PostGIS и Leaflet.markercluster.







