Оптимізація введення адреси: автодоповнення та кешування
Уявіть: користувач вводить «Тверск», система за 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, дебаунс, кеш історії, тестування. Вартість розраховується індивідуально під ваші завдання. Отримайте консультацію — дізнайтеся, скільки займе інтеграція вашого застосунку.







