Developing a store locator (map of stores/offices) on 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Development of Store Locator (Store/Office Map) on 1C-Bitrix

A user enters their city or address and wants to see the nearest store with a route. Standard Bitrix tools — neither bitrix:catalog.element nor trading offers — solve this task. You need a map, geolocation, radius search, and filtering by location type. All this is custom development based on an infoblock and mapping API.

Storing Sales Point Data

Each sales point is an infoblock element. Minimum property set:

  • ADDRESS — full address (string)
  • LAT / LNG — coordinates (numeric properties with 6-digit precision)
  • PHONE — phone (string or multiple property)
  • WORKING_HOURS — working hours (string or JSON: {"mon-fri": "9:00-20:00", "sat": "10:00-18:00"})
  • POINT_TYPE — location type (list: "Store", "Pickup Point", "Service Center")
  • CITY_ID — binding to city reference (city infoblock element)
  • METRO — nearest metro station
  • PHOTO — location photo

Coordinates (LAT/LNG) are key. They're obtained when creating a point: either manually via Yandex.Maps, or via geocoding from address through Yandex Geocoder API when saving the element:

AddEventHandler('iblock', 'OnBeforeIBlockElementAdd', 'geocodeStoreAddress');

function geocodeStoreAddress(&$arFields) {
    if ($arFields['IBLOCK_ID'] !== IBLOCK_STORES_ID) return;
    $address = urlencode($arFields['PROPERTIES']['ADDRESS']['VALUE']);
    $http = new \Bitrix\Main\Web\HttpClient();
    $response = $http->get("https://geocode-maps.yandex.ru/1.x/?apikey=KEY&format=json&geocode=$address");
    $data = json_decode($response, true);
    $coords = $data['response']['GeoObjectCollection']['featureMember'][0]['GeoObject']['Point']['pos'];
    [$lng, $lat] = explode(' ', $coords);
    $arFields['PROPERTIES']['LAT']['VALUE'] = $lat;
    $arFields['PROPERTIES']['LNG']['VALUE'] = $lng;
}

Choosing Mapping API

Yandex.Maps API — priority for Russian audience. Detailed RF coverage, traffic, user geolocation, route building. Pricing: free up to 1,000 requests/day, then commercial plan.

2GIS API — good alternative for Russian and CIS cities, detailed office building maps (floor plans).

OpenStreetMap + Leaflet.js — free option without request limits. Geocoding via Nominatim (free but rate-limited) or paid Geocoding API.

Component Architecture

Store locator consists of two parts:

PHP backend (component.php) — gets list of points from infoblock, optionally filters by city or radius:

// Radius search via haversine formula (approximation)
// For catalogs up to 1000 points — client-side JS filtering is sufficient
$res = CIBlockElement::GetList(
    [],
    ['IBLOCK_ID' => IBLOCK_STORES_ID, 'ACTIVE' => 'Y'],
    false, false,
    ['ID', 'NAME', 'PROPERTY_LAT', 'PROPERTY_LNG', 'PROPERTY_ADDRESS',
     'PROPERTY_PHONE', 'PROPERTY_WORKING_HOURS', 'PROPERTY_POINT_TYPE']
);
$points = [];
while ($el = $res->GetNext()) {
    $points[] = [
        'id'      => $el['ID'],
        'name'    => $el['NAME'],
        'lat'     => (float)$el['PROPERTY_LAT_VALUE'],
        'lng'     => (float)$el['PROPERTY_LNG_VALUE'],
        'address' => $el['PROPERTY_ADDRESS_VALUE'],
        'phone'   => $el['PROPERTY_PHONE_VALUE'],
        'hours'   => $el['PROPERTY_WORKING_HOURS_VALUE'],
        'type'    => $el['PROPERTY_POINT_TYPE_VALUE'],
    ];
}

Data is passed to JS via <script> with JSON or via AJAX endpoint.

JS frontend — initializes map, adds markers, implements search and filtering:

// Yandex.Maps API 3.0
import { load } from '@2gis/mapgl';
// or ymaps3.ready.then(() => { ... })

const map = new ymaps3.YMap(document.getElementById('map'), {
    location: { center: [37.617, 55.755], zoom: 10 }
});

// Clustering for large number of points
const clusterer = new ymaps3.YMapClusterer({ ... });

Marker clustering — mandatory for 50+ points. Without it, the map is overloaded and unusable. Yandex.Maps 3.0 and Leaflet.js (via Leaflet.markercluster plugin) support this out of the box.

Geolocation Search

// Getting browser geolocation
navigator.geolocation.getCurrentPosition((pos) => {
    const userLat = pos.coords.latitude;
    const userLng = pos.coords.longitude;
    // sort points by distance (haversine formula in JS)
    stores.sort((a, b) => distance(userLat, userLng, a.lat, a.lng) - distance(userLat, userLng, b.lat, b.lng));
    renderStoreList(stores.slice(0, 5)); // show 5 nearest
});

If user denies geolocation — fallback: city detection via IP using Sypex Geo (PHP library, databases stored locally) or ip-api.com API.

Sidebar with Store List

Map without sidebar — poor UX. Typical layout: left side store list with distance from user, right side map. Click on store in list centers map and opens balloon. Click on marker highlights card in list.

Synchronization via shared state (simple JS object or small Vue/Alpine.js store):

let selectedStore = null;

function selectStore(id) {
    selectedStore = id;
    // update highlighting in list
    document.querySelectorAll('.store-card').forEach(el => el.classList.remove('active'));
    document.querySelector(`[data-store-id="${id}"]`).classList.add('active');
    // move map
    map.setCenter([stores[id].lng, stores[id].lat], { duration: 300 });
}

Filtering by Location Type and City

Dropdown with cities — SELECT with onChange filtering the stores array client-side and redrawing markers. For 100+ cities, list is unwieldy; better — autocomplete field with search.

Filter by location type (store / pickup / service) — checkboxes hiding/showing corresponding markers via marker.setVisible(false).

Performance with Large Number of Points

With 500+ points, passing the entire array in JS on page load is undesirable — large JSON slows parsing. Solution:

  • Paginated AJAX request: on start, load points within visible map area, on map movement — load new ones
  • Server-side radius search: request PHP with lat, lng, radius params, filter in SQL via haversine formula or PostGIS (if PostgreSQL)
  • Caching: store list rarely changes, aggressive cache for 1 hour in b_cache table

Timeframes

Scale What's Included Timeframe
Up to 50 points Map + list + city filter 1–2 weeks
50–300 points + clustering, geolocation, nearby search 2–4 weeks
300+ points + server-side radius search, PostGIS 4–6 weeks

Store locator — a touchpoint between online and offline. Properly implemented, it reduces call center calls with "where are you located" and boosts footfall conversion.