Setting up geolocation-based nearest store detection in 1C-Bitrix
A user opens the "Our Stores" page and sees a list of 30 addresses with no order. They live in Minsk, but the first five stores are in Moscow. Seemingly trivial task — show the nearest ones — in practice comes down to geolocation accuracy, distance calculation, and storing store coordinates in Bitrix.
Storing store data
In Bitrix stores are kept in two places depending on used functionality:
Module sale, pickup points: table b_sale_store with fields ID, TITLE, ADDRESS, GPS_N (latitude), GPS_S (longitude). This is the standard structure for pickup points.
Info block of stores: if stores are formatted as infoblock elements, coordinates usually stored in custom properties type String or in specialized fields like Map (with third-party modules).
For distance calculation you need numeric coordinates. If they're stored in string property in format "53.9045, 27.5615" — you'll need to parse them during retrieval, which is inconvenient. Right solution: store latitude and longitude in two separate numeric properties or use b_sale_store.GPS_N / b_sale_store.GPS_S.
Determining user location
Two approaches:
Browser geolocation (Geolocation API): accurate to 50 meters but requires user permission. Asynchronous — can't use with server-side rendering.
IP geolocation: works without permission request, accuracy — down to city/district. Bitrix has built-in location module with GeoIP database (b_geocode_city, b_geocode_country). Method \Bitrix\Location\Service\FormatService::getInstance() works with addresses; for IP geolocation use \Bitrix\Main\Service\GeoIp\Manager::getLocationByIp().
$location = \Bitrix\Main\Service\GeoIp\Manager::getLocationByIp(
\Bitrix\Main\Context::getCurrent()->getRequest()->getRemoteAddress()
);
$userLat = $location['LATITUDE'] ?? null;
$userLon = $location['LONGITUDE'] ?? null;
Distance calculation: Haversine formula in SQL
Most efficient approach — calculate distances directly in SQL query. Haversine formula for PostgreSQL:
SELECT
id,
title,
gps_n AS lat,
gps_s AS lon,
(
6371 * acos(
cos(radians(:user_lat)) * cos(radians(gps_n)) *
cos(radians(gps_s) - radians(:user_lon)) +
sin(radians(:user_lat)) * sin(radians(gps_n))
)
) AS distance_km
FROM b_sale_store
WHERE active = 'Y'
AND gps_n IS NOT NULL
AND gps_s IS NOT NULL
ORDER BY distance_km
LIMIT 5;
For MySQL syntax is similar. On PostgreSQL you can additionally use earthdistance extension with cube, which is faster for large point sets.
Via Bitrix ORM direct SQL is called through \Bitrix\Main\Application::getConnection()->query(). D7 ORM has no built-in Haversine expression — you'll need to use ExpressionField with raw SQL or native query.
Frontend: two steps
- On page load — show stores sorted by IP geolocation (server-side sort, instant).
- After getting exact coordinates via
navigator.geolocation.getCurrentPosition()— re-sort via AJAX request to component withlatandlonparameters.
navigator.geolocation.getCurrentPosition(function(pos) {
fetch('/ajax/nearest-stores/?lat=' + pos.coords.latitude + '&lon=' + pos.coords.longitude)
.then(r => r.json())
.then(stores => renderStoreList(stores));
});
AJAX handler component reads lat/lon from GET, runs SQL with Haversine, returns JSON. In Bitrix this is implemented via component with ajax_mode = Y parameter or via custom endpoint in /local/ajax/.
What we configure
- Checking coordinate presence in
b_sale_storeor store infoblock, filling them in - SQL query with Haversine formula via
Application::getConnection() - IP geolocation via
GeoIp\Managerfor initial sort without user permission - AJAX endpoint for re-sorting after getting exact browser coordinates
- Caching results for each coordinate pair (rounded to 0.01 degree)







