אופטימיזציית הזנת כתובת: השלמה אוטומטית וקאשינג
תארו לעצמכם משתמש שמקליד "תל אביב" והמערכת מציעה 5 אפשרויות תוך 300 אלפיות השנייה. ללא יישום נכון, כל הקלדה מפעילה בקשת API נפרדת—70% מהמשתמשים נוטשים את הטופס אם העיכוב עולה על 2 שניות. זמן הזנת כתובת ממוצע עם השלמה אוטומטית הוא 8 שניות, בלעדיה 35 שניות (לפי Nielsen Norman Group). האתגר הטכני הוא לא רק הוספת ספרייה, אלא בחירת הספק, הגדרת debounce, קאשינג ו-session tokens. בואו נפרק איך לעשות את זה נכון.
איך לבחור ספק גיאוקודינג?
| ספק | חוזקות | חולשות |
|---|---|---|
| Google Places Autocomplete API | הכיסוי הגלובלי הטוב ביותר, נקודות עניין, עסקים | יקר בתעבורה גבוהה, חלש יותר במספרי בניינים ברוסיה |
| DaData | הטוב ביותר לכתובות רוסיות (FIAS/KLADR), דיוק >95% | רק רוסיה |
| Nominatim (OpenStreetMap) | חינמי, גלובלי | אין SLA, איטי יותר, איכות נמוכה ב-30% לעומת DaData |
| HERE Geocoding | טוב באירופה, חבילות אופליין | יקר יותר מ-Google לנפחים קטנים |
| Yandex Geocoder | טוב למדינות חבר העמים | דורש חשבון, מגבלות שימוש |
לרוב הפרויקטים הרוסיים, אנו ממליצים על שילוב DaData + Google: DaData כספק ראשי, Google כגיבוי לכתובות זרות. זה מפחית את עלויות הבקשות ב-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: token אחד לכל סשן חיפוש (מהתו הראשון ועד בחירת התוצאה). זה מפחית עלויות פי 3–5 לעומת בקשות ללא token. כפי שתיעוד Google Places קובע: "שימוש ב-session tokens מאפשר קיבוץ של מספר בקשות לחיוב יחיד."
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 }
}לאחר בחירת כתובת, קראו ל-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 } } כדי לקבל קואורדינטות—ואפסו את ה-token. ללא fetchPlace(fromPlaceID:placeFields:sessionToken:), קואורדינטות אינן זמינות מההשלמה האוטומטית.
ב-Android, השתמשו ב-fetchPlace + Places.initialize(context, apiKey). ב-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
} מה נותן Debounce?
ללא debounce, כל הקלדה מפעילה בקשת API. במהירות הקלדה ממוצעת של 4 תווים בשנייה, זה 4 בקשות במקום אחת. הניסיון שלנו מראה ש-debounce נכון של 350 אלפיות השנייה מפחית את מספר הבקשות ב-70%.
יישום debounce שלב אחר שלב:
- צרו Publisher/Flow משדה הטקסט.
- החילו את האופרטור
PlacesClient. - הוסיפו פילטר אורך (>= 3 תווים).
- השתמשו ב-
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 }כדי לבטל את הבקשה הקודמת.
ב-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 עם debounce(for: 350 ms):
searchQuery
.debounce(350)
.filter { it.length >= 3 }
.distinctUntilChanged()
.flatMapLatest { fetchSuggestions(it) }
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())flatMapLatest מבטל את הבקשה הקודמת על קלט חדש—בלעדיו, תוצאות ישנות עלולות לדרוס חדשות.
אופליין וקאש: איך לשפר את חוויית המשתמש?
שמרו את 10–20 הכתובות האחרונות שנבחרו מקומית (UserDefaults / SharedPreferences) והציגו אותן כששדה הקלט ריק. זה פותר את המקרה הנפוץ ביותר: משתמשים שמזמינים משלוח לאותה כתובת.
להיסטוריית חיפוש, השתמשו ב-Room / Core Data עם עמודות 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) } , StateFlow, searchQuery .debounce(350) .filter { it.length >= 3 } .distinctUntilChanged() .flatMapLatest { fetchSuggestions(it) } .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) , flatMapLatest. בעת קלט, חפשו תחילה במסד הנתונים המקומי (שאילתת 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 = ["RU"]
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 }
// сохраняем координаты
}
}
} איך לבדוק השלמה אוטומטית במקרי קצה?
בדיקת השלמה אוטומטית תופסת שגיאות שקשה לזהות בפרודקשן. אנו משתמשים בשכבת mock: מחליפים תגובות API בנתוני בדיקה (תגובה ריקה, עיכובים, שגיאות). בדקו מחרוזות קצה: מחרוזת ריקה, תו בודד, תווים מיוחדים (Room), כתובות ארוכות מאוד (200+ תווים), כתובות עם אותיות לא סטנדרטיות (אומלאוטים, קירילית). כמו כן, דמו כשלי רשת וזמני timeout—האפליקציה צריכה להציג הודעת חלופי בחן ולא לקרוס. יש לנו רשימת בדיקה של 25+ תרחישים לכל פרויקט.
מה כלול בעבודה?
- בחירת ספק ואינטגרציה (DaData, Google, Yandex).
- פיתוח רכיב UI עם רשימה נפתחת (SwiftUI / Jetpack Compose).
- יישום debounce, ביטול בקשות, session tokens.
- קאשינג מקומי של היסטוריית כתובות.
- טיפול בשגיאות: ללא רשת, מגבלות קצב API, קלט לא חוקי.
- בדיקת מחרוזות קצה (ריקות, תווים מיוחדים, כתובות ארוכות מאוד).
- תיעוד אינטגרציה.
יישמנו חיפוש כתובות ב-30+ פרויקטים—משירותי משלוחים ועד מערכות מידע גיאוגרפיות. אנו מבטיחים פעולה יציבה בעומס גבוה. קבלו הערכה לפרויקט שלכם—צרו קשר לייעוץ.
ציר זמן: יומיים עד ארבעה ימים—ספק, UI, debounce, קאש היסטוריה, בדיקות. העלות מחושבת באופן אישי למשימות שלכם. קבלו ייעוץ כדי לגלות כמה זמן תיקח האינטגרציה של האפליקציה שלכם.







