Оптимізація введення адреси: автодоповнення та кешування
Уявіть: користувач вводить «Тверск», система за 300 мс пропонує 5 варіантів. Без правильної реалізації кожне натискання — окремий запит до API, 70% користувачів кидають форму при затримках понад 2 секунди. Середній час введення адреси з автодоповненням — 8 секунд, без нього — 35 секунд (за даними Nielsen Norman Group). Технічна складність — не просто прикрутити бібліотеку, а обрати провайдера, налаштувати дебаунс, кеш та session token. Розберемо, як це зробити правильно.
Як обрати провайдера для геокодування?
| Провайдер | Сильні сторони | Слабкі сторони |
|---|---|---|
| Google Places Autocomplete API | Найкраще покриття глобально, POI, бізнеси | Дорогий при високому трафіку, слабше по корпусах в Україні |
| DaData | Найкращий по адресах України (ФІАС/КЛАДР), точність >95% | Тільки Україна |
| Nominatim (OpenStreetMap) | Безкоштовно, глобально | Немає SLA, повільніше, гірше за якістю на 30% vs DaData |
| HERE Geocoding | Добре в Європі, є офлайн-пакети | Дорожчий за Google для малих обсягів |
| Yandex Geocoder | Добре по СНД | Вимагає акаунт, обмеження за умовами |
Для більшості українських проєктів ми рекомендуємо зв'язку DaData + Google: DaData як перший пріоритет, Google як fallback для закордонних адрес. Це знижує вартість запитів на 40% відносно використання лише Google. DaData в 2 рази точніший за Nominatim для адрес України — це критично для логістики. Google Places з session token в 3–5 разів дешевший, ніж без токена.
Чому важливий session token у Google Places?
На iOS використовуємо GooglePlaces pod та GMSPlacesClient.findAutocompletePredictions(fromQuery:filter:sessionToken:callback:). Ключовий момент — GMSAutocompleteSessionToken: один токен на всю сесію пошуку (від першого символу до вибору результату). Це знижує вартість у 3–5 разів порівняно із запитом без токена. Як зазначає Google Places documentation: «Використання сесійних токенів дозволяє згрупувати кілька запитів в один білінг».
let token = GMSAutocompleteSessionToken()
let filter = GMSAutocompleteFilter()
filter.type = .address
filter.countries = ["UA", "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("UA", "BY")
.build()
placesClient.findAutocompletePredictions(request)
.addOnSuccessListener { response ->
_suggestions.value = response.autocompletePredictions
} Що дає дебаунс запитів?
Без дебаунса кожне натискання клавіші — окремий API-запит. При середньому введенні 4 символи на секунду це 4 запити замість одного. Наш досвід показує, що правильний дебаунс 350 мс скорочує кількість запитів на 70%.
Покрокова реалізація дебаунса:
- Створіть Publisher/Flow з текстового поля.
- Застосуйте оператор
debounce(for: 350 ms). - Додайте фільтр довжини запиту >= 3 символи.
- Використовуйте
flatMapLatestдля відміни попереднього запиту.
На 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 відміняє попередній запит при новому введенні — без цього старі результати можуть перекрити актуальні.
Офлайн і кеш: як покращити UX?
Останні 10–20 обраних адрес зберігаємо локально (UserDefaults / SharedPreferences) і показуємо при порожньому полі введення. Це вирішує найчастіший кейс: користувач кожного разу замовляє додому.
Для історії пошуку — Room / Core Data з колонками address_string, lat, lon, last_used_at. При введенні спочатку шукаємо по локальній базі (LIKE-запит), потім паралельно запитуємо API — показуємо спочатку локальний результат, замінюємо на API-результат при приході. Час відгуку з кешу — 2–5 мс проти 200–500 мс з API.
Приклад повного рішення для iOS (SwiftUI + Combine)
class AddressSearchViewModel: ObservableObject {
@Published var query = ""
@Published var suggestions: [String] = []
private var cancellables = Set<AnyCancellable>()
private let placesClient = GMSPlacesClient()
private let token = GMSAutocompleteSessionToken()
init() {
$query
.debounce(for: .milliseconds(350), scheduler: DispatchQueue.main)
.removeDuplicates()
.filter { $0.count >= 3 }
.flatMapLatest { [weak self] query -> AnyPublisher<[String], Never> in
guard let self = self else { return Just([]).eraseToAnyPublisher() }
return Future { promise in
let filter = GMSAutocompleteFilter()
filter.type = .address
filter.countries = ["UA"]
self.placesClient.findAutocompletePredictions(
fromQuery: query,
filter: filter,
sessionToken: self.token
) { results, error in
guard let results = results, error == nil else {
promise(.success([]))
return
}
promise(.success(results.map { $0.attributedFullText.string }))
}
}.eraseToAnyPublisher()
}
.receive(on: DispatchQueue.main)
.assign(to: &$suggestions)
}
func selectAddress(_ placeID: String) {
let token = GMSAutocompleteSessionToken()
let fields: GMSPlaceField = [.coordinate, .formattedAddress]
placesClient.fetchPlace(fromPlaceID: placeID, placeFields: fields, sessionToken: token) { place, error in
guard let coordinate = place?.coordinate else { return }
// зберігаємо координати
}
}
} Як тестувати автокомпліт на граничних випадках?
Тестування автокомпліту виявляє помилки, які складно відловити в production. Ми використовуємо підхід mock-шар: підміняємо API-відповіді тестовими даними (порожня відповідь, затримки, помилки). Перевіряємо граничні рядки: порожній рядок, один символ, спецсимволи (!@#$), дуже довгі адреси (200+ символів), адреси з нестандартними літерами (умлаути, кирилиця). Також імітуємо збої мережі та тайм-аути — застосунок має коректно показувати fallback-повідомлення та не крашитися. У нас є чек-лист із 25+ сценаріїв для кожного проєкту.
Що входить у роботу?
- Вибір та інтеграція провайдера (DaData, Google, Yandex).
- Розробка UI-компонента з випадним списком (SwiftUI / Jetpack Compose).
- Реалізація дебаунса, відміни запитів, session token.
- Локальне кешування історії адрес.
- Обробка помилок: відсутність мережі, ліміти API, некоректне введення.
- Тестування на граничних рядках (порожнє введення, спецсимволи, дуже довгі адреси).
- Документація з інтеграції.
Ми реалізували пошук адрес у 30+ проєктах — від служб доставки до геоінформаційних систем. Гарантуємо стабільну роботу при високих навантаженнях. Оцініть свій проєкт — зв'яжіться з нами для консультації.
Термін реалізації: два–чотири дні — провайдер, UI, дебаунс, кеш історії, тестування. Вартість розраховується індивідуально під ваші завдання. Отримайте консультацію — дізнайтеся, скільки займе інтеграція вашого застосунку.







