Реализация поиска по адресу с подсказками в мобильном приложении
Поле ввода адреса с подсказками — один из самых конверсионных UI-элементов в приложениях доставки и логистики. Пользователь вводит «Тверск» и за 300 миллисекунд видит список вариантов. Технически за этим стоит выбор провайдера, дебаунс, кэширование сессий и правильная обработка выбора.
Провайдеры и когда что выбирать
| Провайдер | Сильные стороны | Слабые стороны |
|---|---|---|
| Google Places Autocomplete API | Лучшее покрытие глобально, POI, бизнесы | Дорого при высоком трафике, слабее по корпусам в РФ |
| DaData | Лучший по адресам России (ФИАС/КЛАДР) | Только РФ |
| Nominatim (OpenStreetMap) | Бесплатно, глобально | Нет SLA, медленнее, хуже по качеству |
| HERE Geocoding | Хорошо в Европе, есть офлайн-пакеты | Дороже Google для малых объёмов |
| Yandex Geocoder | Хорошо по СНГ | Требует аккаунт, ограничения по условиям |
Для большинства российских проектов — связка DaData + Google: DaData как первый приоритет, Google как fallback для зарубежных адресов.
Google Places SDK: правильная интеграция
На iOS — GooglePlaces pod. Используем GMSPlacesClient.findAutocompletePredictions(fromQuery:filter:sessionToken:callback:). Ключевой момент — GMSAutocompleteSessionToken: один токен на всю сессию поиска (от первого символа до выбора результата). Это снижает стоимость в 3-5 раз по сравнению с запросом без токена.
let token = GMSAutocompleteSessionToken()
let filter = GMSAutocompleteFilter()
filter.type = .address
filter.countries = ["RU", "BY", "KZ"]
placesClient.findAutocompletePredictions(
fromQuery: query,
filter: filter,
sessionToken: token
) { results, error in
guard let results else { return }
self.suggestions = results.map { $0.attributedFullText.string }
}
После выбора адреса вызываем fetchPlace(fromPlaceID:placeFields:sessionToken:) для получения координат — и обнуляем токен. Без fetchPlace координаты не получить через автодополнение.
На Android — Places.initialize(context, apiKey) + PlacesClient. В Jetpack Compose:
val placesClient = Places.createClient(context)
val request = FindAutocompletePredictionsRequest.builder()
.setQuery(query)
.setSessionToken(AutocompleteSessionToken.newInstance())
.setTypesFilter(listOf(PlaceTypes.ADDRESS))
.setCountries("RU", "BY")
.build()
placesClient.findAutocompletePredictions(request)
.addOnSuccessListener { response ->
_suggestions.value = response.autocompletePredictions
}
Дебаунс и UX-детали
Без дебаунса каждое нажатие клавиши — отдельный API-запрос. При среднем вводе 4 символа в секунду это 4 запроса вместо одного.
На iOS через Combine:
searchTextField.textPublisher
.debounce(for: .milliseconds(350), scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] query in
guard query.count >= 3 else { return }
self?.fetchSuggestions(for: query)
}
На Android через StateFlow:
searchQuery
.debounce(350)
.filter { it.length >= 3 }
.distinctUntilChanged()
.flatMapLatest { fetchSuggestions(it) }
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
flatMapLatest отменяет предыдущий запрос при новом вводе — без этого старые результаты могут перекрыть актуальные.
Офлайн и кэш
Последние 10-20 выбранных адресов храним локально (UserDefaults / SharedPreferences) и показываем при пустом поле ввода. Это решает самый частый кейс: пользователь каждый раз заказывает домой.
Для истории поиска — Room / Core Data с колонками address_string, lat, lon, last_used_at. При вводе сначала ищем по локальной базе (LIKE-запрос), потом параллельно запрашиваем API — показываем сначала локальный результат, заменяем на API-результат при приходе.
Срок реализации: два-четыре дня — провайдер, UI-компонент, дебаунс, кэш истории, тестирование на граничных строках.







