Уведомление, которое приходит на телефон за минуту до прохода мимо кофейни, не читает мысли: оно читает координаты. Геофенсинг - это виртуальная окружность вокруг точки на карте, и когда устройство пересекает её границу, срабатывает событие. Механика старая и точная, но между идеей "предложение рядом с магазином" и работающей кампанией лежит инженерия: выбрать радиус, который переживёт городскую погрешность GPS, настроить триггеры входа и ожидания, уложиться в лимиты платформ и в батарею телефона. Разбор ниже идёт от формулы расстояния до правил приличия, без которых локальные уведомления превращаются в спам, от которого страдает репутация бренда.
Виртуальная окружность вокруг точки и три события на её границе
Геозона математически скромна: широта, долгота и радиус в метрах. Система позиционирования непрерывно сравнивает текущую точку с центром окружности и присваивает устройству состояние внутри или снаружи. На смене состояний работают три события: вход в зону, выход из зоны и ожидание внутри зоны. Три события закрывают три разные маркетинговые механики: вход будит проходящего мимо, выход ловит того, кто ушёл без покупки, ожидание отделяет настоящего посетителя от человека, который просто переходил улицу.
Ожидание заслуживает отдельного внимания, потому что именно оно спасает от главного бича локальных кампаний - уведомлений, которые стреляют по всем, кто оказался в радиусе. У события ожидания есть собственный порог: система ждёт, пока устройство проведёт внутри зоны заданное количество секунд, и только потом будит приложение. Прохожий идёт со скоростью около 1.4 метра в секунду и покидает окружность радиуса 150 метров меньше чем за две минуты, если зона не была его целью. Время ожидания в 5 минут превращает геофенсинг из пугача в фильтр.
У зоны есть и срок жизни: время истечения задаётся в миллисекундах при регистрации, а специальная константа NEVER_EXPIRE оставляет зону бессрочной. Кампания на выходные регистрируется до пятницы и умирает сама в воскресенье ночью, без ручного снятия.
Реализация на Android через GeofencingClient сервиса Google Play
На Android за геофенсингом стоит клиент из пакета сервисов Google Play. Приложение регистрирует зоны, а следит за ними отдельный системный процесс com.google.process.location пакета com.google.android.gms: приложение можно выгрузить из памяти, события всё равно доставятся через PendingIntent, а система при необходимости поднимет процесс заново. Ограничения платформы суровые, но честные: 100 геофенсов на приложение, а на многопользовательских устройствах лимит считается на каждого пользователя отдельно. Код регистрации умещается в экран:
val client = LocationServices.getGeofencingClient(context)
val fence = Geofence.Builder()
.setRequestId("cafe-arbat-01")
.setCircularRegion(55.7520, 37.6175, 150.0) // радиус в метрах
.setExpirationDuration(12 * 60 * 60 * 1000) // 12 часов жизни
.setTransitionTypes(
Geofence.GEOFENCE_TRANSITION_ENTER or
Geofence.GEOFENCE_TRANSITION_EXIT
)
.build()
val request = GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofence(fence)
.build()
client.addGeofences(request, pendingIntent)
Идентификатор зоны - строка, и её полезно делать говорящей: идентификатор точки плюс индекс варианта при A/B проверке радиусов. Флаг начального триггера решает, что произойдёт при регистрации, если телефон уже внутри окружности: событие входа придёт сразу, и приложение, перерегистрировавшее зоны после перезагрузки устройства, не пропустит людей, которые всё это время стояли внутри.
Перезагрузка устройства стирает зоны, поэтому приложение перерегистрирует их при старте - это не недоработка платформы, а осознанная гигиена: перезагруженный телефон один раз подтвердит набор зон и дальше спит.
Реализация на iOS через Core Location и лимит двадцати зон
Apple решает ту же задачу своим фреймворком Core Location, и лимит здесь ещё жёстче: 20 зон на приложение. Зона описывается классом CLCircularRegion, события приходят в делегаты didEnterRegion и didExitRegion, а доставка работает и для выгруженных приложений - система сама поднимет процесс в фоне:
let center = CLLocationCoordinate2D(latitude: 55.7520, longitude: 37.6175)
let region = CLCircularRegion(
center: center,
radius: 150.0,
identifier: "cafe-arbat-01"
)
region.notifyOnEntry = true
region.notifyOnExit = true
locationManager.startMonitoring(for: region)
func locationManager(
_ manager: CLLocationManager,
didEnterRegion region: CLRegion
) {
// телефон пересёк границу зоны снаружи внутрь
}
Двадцать зон означают приоритизацию: сетевая кофейня с сотней точек не регистрирует всё сразу, а держит ближайшие к дому и работе человека зоны, ротацию делает сервер. Вторая особенность платформы - авторизация точности: начиная с iOS 14 пользователь может выдать приложению приближённое разрешение, при котором координаты приходят из грубых сетевых ячеек размером с целый квартал. Приложение обязано уметь жить в обоих режимах, а маркетингу остаётся честно объяснять в диалоге запроса, зачем нужна точная геолокация.
Документация Apple рекомендует радиус от 100 метров: для меньших окружностей система не гарантирует доставку событий, ведь экономия батареи важнее отдельного маркетингового сценария.
Точность GPS на городских улицах и выбор радиуса зоны
Геометрия зоны проста, пока в дело не вступает физика. Спутниковый сигнал в плотной застройке отражается от стен, и погрешность позиционирования в городе легко достигает десятков метров; внутри зданий приёмник часто теряет спутники вовсе. Радиус в 30 метров на практике означает, что часть прохожих на другой стороне улицы будет числиться внутри зоны, а часть находящихся в магазине - снаружи. Полезно чувствовать масштаб в градусах: 0.001 градуса широты - это 111 метров, а 0.001 градуса долготы на широте Москвы - уже 63 метра, и разница между координатами 55.7520, 37.6175 и 55.7562, 37.6230 составляет 580 метров.
Расстояние между точками на сфере считается по формуле гаверсинуса, и на веб-странице она же превращает браузерную геолокацию в самодельный геофенсинг:
function distanceM(lat1, lon1, lat2, lon2) {
const R = 6371000; // средний радиус Земли в метрах
const rad = (d) => (d * Math.PI) / 180;
const dLat = rad(lat2 - lat1);
const dLon = rad(lon2 - lon1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(rad(lat1)) * Math.cos(rad(lat2)) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(a));
}
// 55.7520, 37.6175 -> 55.7562, 37.6230 даёт 580 метров
const inside = distanceM(userLat, userLon, shopLat, shopLon) <= RADIUS;
Браузерный вариант строится на методе watchPosition объекта геолокации: флаг enableHighAccuracy просит точный приём, таймаут ограничивает ожидание, maximumAge разрешает вернуть свежий кэш. У границы вводят гистерезис: вход засчитывается на расстоянии радиус минус 20 метров, выход - на радиус плюс 20, иначе дрожащая позиция у кромки зоны породит очередь ложных событий.
Практические радиусы скромнее амбиций: 100-150 метров для точки в плотной застройке, 300-500 для крупных торговых центров и зон у шоссе, а событие ожидания прикрывает оставшуюся погрешность. Время пересечения связано с радиусом арифметикой: пешеход со скоростью 1.4 метра в секунду проходит окружность радиусом 150 метров меньше чем за четыре минуты, и это подсказывает разумный порог ожидания.
Расход батареи отзывчивость уведомлений и интервалы проверки
Непрерывный опрос GPS высаживает батарею за часы, поэтому платформы делают геофенсинг событийным: система сама решает, когда проверить позицию, и будит приложение только на границе зоны. Документация Android прямо торгуется: параметр отзывчивости уведомлений позволяет поднять интервал проверки, и значение в пять минут означает, что система будет сверяться с границей зоны раз в пять минут - экономия батареи ценой задержки события. Для кафе с витриной задержка в пару минут безразлична, а для напоминания у кассы - критична.
Три экономных канала позиции выстраиваются по цене: сотовые вышки дают район, Wi-Fi точки - квартал, спутники - метры. Умное позиционирование перебирает их от дешёвых к точным, и в маркетинге это оборачивается стратегией: зона объявлений широкая и нетребовательная, зона триггера продажи - узкая и событийная. В браузере тоже надо знать меру: слушатель watchPosition снимают при скрытии страницы, иначе фоновая вкладка будет жаловаться на прожорливость.
Сценарии локальных предложений без раздражения клиента
Техника техника техникой, но репутацию кампании делают правила приличия. Классические сценарии работают на ритме дня: утренний кофе по пути на работу, обеденное меню в полдень, спортивный зал по соседству вечером пятницы, магазин у метро в час пик. Событие ожидания отрезает прохожих, событие выхода ловит ушедших без покупки, а время внутри зоны подсказывает сценарий: человек у стойки тридцать минут - это не тот, кому нужен купон на входе.
Частота - главный регулятор: лимит в одно уведомление в три дня на человека, тихие часы после 22 и до 9, отдельный выключатель геосообщений в настройках приложения. Отписка от пула - красный флаг: кампания не спасёт воронку, если её сообщения сливаются со спамом. Радиусы тестируются параллельно: половина аудитории получает зону в 150 метров, половина - в 300, и сравнение конверсий честнее любых теорий.
Метрики кампании читают по трём линиям: доля доставленных уведомлений, конверсия уведомление-визит и конверсия визит-покупка, рядом держат долю отписок. Визит засчитывают по повторному входу в зону в течение часа после показа, покупку - по номеру карты лояльности. Дисциплина измерений важнее количества метрик: сравнение с контрольной аудиторией без геосообщений показывает прирост, а не суточный шум колебаний.
Серверная проверка координат и защита от фиктивной геолокации
Награда за вход в зону рождает и накрутку: приложения для подмены координат позволяют "посетить" кафе, не вставая с дивана. Первая линия защиты - клиентская честность: Play Integrity API подтверждает, что событие пришло из неподделанной сборки приложения на реальном устройстве. Вторая линия - серверная физика: последовательные точки проверяются на правдоподобие скорости. Пять километров за минуту - это 83 метра в секунду, вдвое быстрее спринтера мирового класса; пешеход живёт в диапазоне до пары метров в секунду, и любой прыжок между точками за пределами физики отбраковывается:
import math
R = 6371000.0
def distance_m(p1, p2):
lat1, lon1 = p1
lat2, lon2 = p2
rad = math.radians
a = math.sin(rad(lat2 - lat1) / 2) ** 2 + math.cos(rad(lat1)) * math.cos(rad(lat2)) * math.sin(rad(lon2 - lon1) / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
def is_plausible(prev, cur, dt):
speed = distance_m(prev, cur) / max(dt, 1.0)
return speed < 55.0 # быстрее спринтера - фикция
Решение о награде принимает сервер, а не клиент: телефон лишь приносит событие, факты проверяются против истории точек, и подозрительный трафик уходит в карантин вместо купона. Дешёвая проверка вычисляется на лету, дорогие проверки целостности включаются для сомнительных устройств точечно.
Перед запуском кампании разметку зоны прогоняют по короткому списку:
- Определить точку интереса и радиус от 100 до 300 метров с поправкой на плотность застройки;
- Выбрать триггер: вход для витрин, ожидание для заведений, выход для возврата;
- Назначить окно активности и тихие часы в настройках кампании;
- Ограничить частоту одним уведомлением в три дня на человека;
- Замерить конверсию контрольной группы с выключенным геофенсингом.
Геофенсинг в чистом виде - это не маркетинговая уловка, а аккуратная привязка момента к месту. Технология выросла из желания показывать человеку то, что полезно именно здесь и сейчас, и держится на трёх китах: честный радиус, событие с физическим смыслом и уважение к батарее с тишиной в кармане. Кампания, которая соблюдает эту дисциплину, превращает координаты в конверсии, а кампания, которая её игнорирует, превращает лояльных гостей в отписки.