bookalimo 1.0.0__py3-none-any.whl → 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,194 +4,40 @@ Common utilities and shared functionality for Google Places clients.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- from typing import Literal, Sequence, Union
7
+ import re
8
+ from typing import Any, Awaitable, Callable, Optional, cast
8
9
 
10
+ import httpx
11
+ from google.api_core import exceptions as gexc
12
+
13
+ from ...exceptions import BookalimoError
9
14
  from ...logging import get_logger
15
+ from ...schemas.places import (
16
+ AddressComponent,
17
+ AddressDescriptor,
18
+ FieldMaskInput,
19
+ GooglePlace,
20
+ Place,
21
+ PlaceType,
22
+ RankPreference,
23
+ compile_field_mask,
24
+ )
25
+ from ...schemas.places import (
26
+ google as models,
27
+ )
28
+ from .proto_adapter import validate_proto_to_model
29
+ from .resolve_airport import resolve_airport
10
30
 
11
31
  logger = get_logger("places")
12
32
 
33
+ # Type variables for shared functionality
13
34
 
14
- Fields = Union[
15
- str,
16
- Sequence[
17
- Literal[
18
- "*",
19
- # Identity
20
- "name",
21
- # Labels & typing
22
- "display_name",
23
- "types",
24
- "primary_type",
25
- "primary_type_display_name",
26
- # Phones & addresses
27
- "national_phone_number",
28
- "international_phone_number",
29
- "formatted_address",
30
- "short_formatted_address",
31
- "postal_address",
32
- "address_components",
33
- "plus_code",
34
- # Location & map
35
- "location",
36
- "viewport",
37
- # Scores, links, media
38
- "rating",
39
- "google_maps_uri",
40
- "website_uri",
41
- "reviews",
42
- "photos",
43
- # Hours
44
- "regular_opening_hours",
45
- "current_opening_hours",
46
- "current_secondary_opening_hours",
47
- "regular_secondary_opening_hours",
48
- "utc_offset_minutes",
49
- "time_zone",
50
- # Misc attributes
51
- "adr_format_address",
52
- "business_status",
53
- "price_level",
54
- "attributions",
55
- "user_rating_count",
56
- "icon_mask_base_uri",
57
- "icon_background_color",
58
- # Food/venue features
59
- "takeout",
60
- "delivery",
61
- "dine_in",
62
- "curbside_pickup",
63
- "reservable",
64
- "serves_breakfast",
65
- "serves_lunch",
66
- "serves_dinner",
67
- "serves_beer",
68
- "serves_wine",
69
- "serves_brunch",
70
- "serves_vegetarian_food",
71
- "outdoor_seating",
72
- "live_music",
73
- "menu_for_children",
74
- "serves_cocktails",
75
- "serves_dessert",
76
- "serves_coffee",
77
- "good_for_children",
78
- "allows_dogs",
79
- "restroom",
80
- "good_for_groups",
81
- "good_for_watching_sports",
82
- # Options & related places
83
- "payment_options",
84
- "parking_options",
85
- "sub_destinations",
86
- "accessibility_options",
87
- # Fuel/EV & AI summaries
88
- "fuel_options",
89
- "ev_charge_options",
90
- "generative_summary",
91
- "review_summary",
92
- "ev_charge_amenity_summary",
93
- "neighborhood_summary",
94
- # Context
95
- "containing_places",
96
- "pure_service_area_business",
97
- "address_descriptor",
98
- "price_range",
99
- # Missing in your model but present in proto
100
- "editorial_summary",
101
- ]
102
- ],
103
- ]
104
-
105
- PlaceListFields = Union[
106
- str,
107
- Sequence[
108
- Literal[
109
- "*",
110
- # Identity
111
- "places.name",
112
- # Labels & typing
113
- "places.display_name",
114
- "places.types",
115
- "places.primary_type",
116
- "places.primary_type_display_name",
117
- # Phones & addresses
118
- "places.national_phone_number",
119
- "places.international_phone_number",
120
- "places.formatted_address",
121
- "places.short_formatted_address",
122
- "places.postal_address",
123
- "places.address_components",
124
- "places.plus_code",
125
- # Location & map
126
- "places.location",
127
- "places.viewport",
128
- # Scores, links, media
129
- "places.rating",
130
- "places.google_maps_uri",
131
- "places.website_uri",
132
- "places.reviews",
133
- "places.photos",
134
- # Hours
135
- "places.regular_opening_hours",
136
- "places.current_opening_hours",
137
- "places.current_secondary_opening_hours",
138
- "places.regular_secondary_opening_hours",
139
- "places.utc_offset_minutes",
140
- "places.time_zone",
141
- # Misc attributes
142
- "places.adr_format_address",
143
- "places.business_status",
144
- "places.price_level",
145
- "places.attributions",
146
- "places.user_rating_count",
147
- "places.icon_mask_base_uri",
148
- "places.icon_background_color",
149
- # Food/venue features
150
- "places.takeout",
151
- "places.delivery",
152
- "places.dine_in",
153
- "places.curbside_pickup",
154
- "places.reservable",
155
- "places.serves_breakfast",
156
- "places.serves_lunch",
157
- "places.serves_dinner",
158
- "places.serves_beer",
159
- "places.serves_wine",
160
- "places.serves_brunch",
161
- "places.serves_vegetarian_food",
162
- "places.outdoor_seating",
163
- "places.live_music",
164
- "places.menu_for_children",
165
- "places.serves_cocktails",
166
- "places.serves_dessert",
167
- "places.serves_coffee",
168
- "places.good_for_children",
169
- "places.allows_dogs",
170
- "places.restroom",
171
- "places.good_for_groups",
172
- "places.good_for_watching_sports",
173
- # Options & related places
174
- "places.payment_options",
175
- "places.parking_options",
176
- "places.sub_destinations",
177
- "places.accessibility_options",
178
- # Fuel/EV & AI summaries
179
- "places.fuel_options",
180
- "places.ev_charge_options",
181
- "places.generative_summary",
182
- "places.review_summary",
183
- "places.ev_charge_amenity_summary",
184
- "places.neighborhood_summary",
185
- # Context
186
- "places.containing_places",
187
- "places.pure_service_area_business",
188
- "places.address_descriptor",
189
- "places.price_range",
190
- # Missing in your model but present in proto
191
- "places.editorial_summary",
192
- ]
193
- ],
194
- ]
35
+ # Default field mask for places queries
36
+ DEFAULT_PLACE_FIELDS = (
37
+ "display_name",
38
+ "formatted_address",
39
+ "location",
40
+ )
195
41
 
196
42
  ADDRESS_TYPES = {
197
43
  "street_address",
@@ -212,34 +58,452 @@ ADDRESS_TYPES = {
212
58
  }
213
59
 
214
60
 
215
- def fmt_exc(e: BaseException) -> str:
61
+ def _fmt_exc(e: BaseException) -> str:
216
62
  """Format exception for logging without touching non-existent attributes."""
217
63
  return f"{type(e).__name__}: {e}"
218
64
 
219
65
 
220
- def mask_header(fields: Sequence[str] | str | None) -> tuple[tuple[str, str], ...]:
66
+ def _mask_header(
67
+ fields: FieldMaskInput, prefix: str = ""
68
+ ) -> tuple[tuple[str, str], ...]:
221
69
  """
222
70
  Build the X-Goog-FieldMask header. Pass a comma-separated string or a sequence.
223
71
  If None, no header is added (e.g., autocomplete, get_photo_media).
224
72
  """
225
73
  if fields is None:
226
- return ()
227
- if isinstance(fields, str):
228
- value = fields
74
+ return () # type: ignore[unreachable]
75
+ value = compile_field_mask(fields, prefix=prefix)
76
+ return (("x-goog-fieldmask", ",".join(value)),)
77
+
78
+
79
+ def strip_html(s: str) -> str:
80
+ # Simple fallback for adr_format_address (which is HTML)
81
+ return re.sub(r"<[^>]+>", "", s) if s else s
82
+
83
+
84
+ def infer_place_type(m: GooglePlace) -> PlaceType:
85
+ # 1) Airport wins outright
86
+ tset = set(m.types or [])
87
+ ptype = (m.primary_type or "").lower() if getattr(m, "primary_type", None) else ""
88
+ if "airport" in tset or ptype == "airport":
89
+ return PlaceType.AIRPORT
90
+ # 2) Anything that looks like a geocoded address
91
+ if tset & ADDRESS_TYPES:
92
+ return PlaceType.ADDRESS
93
+ # 3) Otherwise treat as a point of interest
94
+ return PlaceType.POI
95
+
96
+
97
+ def get_lat_lng(model: GooglePlace) -> tuple[float, float]:
98
+ if model.location:
99
+ lat = model.location.latitude
100
+ lng = model.location.longitude
101
+ elif model.viewport:
102
+ lat = model.viewport.low.latitude
103
+ lng = model.viewport.low.longitude
229
104
  else:
230
- value = ",".join(fields)
231
- return (("x-goog-fieldmask", value),)
105
+ lat = 0
106
+ lng = 0
107
+ return lat, lng
232
108
 
233
109
 
234
- # Default field mask for places queries
235
- DEFAULT_PLACE_FIELDS: Fields = (
236
- "display_name",
237
- "formatted_address",
238
- "location",
239
- )
110
+ def name_from_place(p: Place) -> Optional[str]:
111
+ # Try common fields; support Google Places v1 structure where display_name may have `.text`
112
+ gp = getattr(p, "google_place", None)
113
+ candidates = [
114
+ getattr(gp, "display_name", None),
115
+ getattr(gp, "name", None),
116
+ getattr(p, "name", None),
117
+ getattr(p, "address_descriptor", None),
118
+ getattr(p, "address_components", None),
119
+ getattr(p, "formatted_address", None),
120
+ ]
121
+ for c in candidates:
122
+ if not c:
123
+ continue
240
124
 
241
- DEFAULT_PLACE_LIST_FIELDS: PlaceListFields = (
242
- "places.display_name",
243
- "places.formatted_address",
244
- "places.location",
245
- )
125
+ maybe_text = getattr(c, "text", c)
126
+ if isinstance(maybe_text, str) and maybe_text.strip():
127
+ return maybe_text.strip()
128
+ elif isinstance(maybe_text, AddressDescriptor):
129
+ return maybe_text.landmarks[0].display_name.text
130
+ elif isinstance(maybe_text, list):
131
+ for item in maybe_text:
132
+ if isinstance(item, AddressComponent):
133
+ return item.long_text
134
+ else:
135
+ break
136
+ return None
137
+
138
+
139
+ def _build_search_request_params(
140
+ query: Optional[str],
141
+ request: Optional[models.SearchTextRequest],
142
+ **kwargs: Any,
143
+ ) -> dict[str, Any]:
144
+ """Build request parameters for search_text API call."""
145
+ if query is None and request is None:
146
+ raise ValueError("Either 'query' or 'request' must be provided")
147
+ if query is not None and request is not None:
148
+ raise ValueError(
149
+ "Only one of 'query' or 'request' should be provided, not both"
150
+ )
151
+
152
+ if request is not None:
153
+ request_params = request.model_dump(exclude_none=True)
154
+ request_params.update(kwargs)
155
+ return request_params
156
+ else:
157
+ return {"text_query": query, **kwargs}
158
+
159
+
160
+ def _normalize_place_from_proto(proto: Any) -> models.Place:
161
+ """Convert a proto Place to our normalized Place model."""
162
+
163
+ model = validate_proto_to_model(proto, GooglePlace)
164
+ adr_format = getattr(model, "adr_format_address", None)
165
+ addr = (
166
+ getattr(model, "formatted_address", None)
167
+ or getattr(model, "short_formatted_address", None)
168
+ or strip_html(adr_format or "")
169
+ or ""
170
+ )
171
+ lat, lng = get_lat_lng(model)
172
+ return models.Place(
173
+ formatted_address=addr,
174
+ lat=lat,
175
+ lng=lng,
176
+ place_type=infer_place_type(model),
177
+ google_place=model,
178
+ )
179
+
180
+
181
+ def _normalize_search_results(proto_response: Any) -> list[models.Place]:
182
+ """Convert search_text response to list of normalized Place models."""
183
+ return [_normalize_place_from_proto(proto) for proto in proto_response.places]
184
+
185
+
186
+ def _build_get_place_request(
187
+ place_id: Optional[str], request: Optional[models.GetPlaceRequest]
188
+ ) -> dict[str, Any]:
189
+ """Build request parameters for get_place API call."""
190
+ if place_id is None and request is None:
191
+ raise ValueError("Either 'place_id' or 'request' must be provided")
192
+
193
+ if request:
194
+ return request.model_dump(exclude_none=True, context={"enum_out": "name"})
195
+ else:
196
+ return {"name": f"places/{place_id}"}
197
+
198
+
199
+ def _derive_effective_query(query: Optional[str], places: list[models.Place]) -> str:
200
+ """Derive an effective query string from query and/or places."""
201
+ effective_query = (query or "").strip()
202
+
203
+ if not effective_query:
204
+ if len(places) == 1:
205
+ derived = name_from_place(places[0])
206
+ if not derived:
207
+ raise ValueError("Could not derive a query from the provided place.")
208
+ effective_query = derived
209
+ else:
210
+ raise ValueError(
211
+ "Multiple places provided but no 'query' to disambiguate them."
212
+ )
213
+
214
+ return effective_query
215
+
216
+
217
+ def _validate_resolve_airport_inputs(
218
+ place_id: Optional[str],
219
+ places: Optional[list[models.Place]],
220
+ max_distance_km: Optional[float],
221
+ ) -> None:
222
+ """Validate inputs for resolve_airport method."""
223
+ if place_id is not None and places is not None:
224
+ raise ValueError("Provide only one of place_id or places, not both.")
225
+ if max_distance_km is not None and max_distance_km <= 0:
226
+ raise ValueError("max_distance_km, if provided, must be > 0.")
227
+
228
+
229
+ def validate_autocomplete_inputs(
230
+ input: Optional[str],
231
+ request: Optional[models.AutocompletePlacesRequest],
232
+ ) -> models.AutocompletePlacesRequest:
233
+ """Validate inputs for autocomplete method."""
234
+ if request is None:
235
+ if input is None:
236
+ raise ValueError("Either input or request must be provided.")
237
+ else:
238
+ request = models.AutocompletePlacesRequest(input=input)
239
+ return request
240
+
241
+
242
+ def _call_gplaces_sync(
243
+ ctx: str,
244
+ fn: Callable[..., Any],
245
+ *args: Any,
246
+ not_found_ok: bool = False,
247
+ **kwargs: Any,
248
+ ) -> Any:
249
+ try:
250
+ return fn(*args, **kwargs)
251
+ except gexc.NotFound:
252
+ if not_found_ok:
253
+ return None
254
+ raise BookalimoError(f"{ctx} not found") from None
255
+ except gexc.InvalidArgument as e:
256
+ msg = f"{ctx} invalid argument: {_fmt_exc(e)}"
257
+ logger.error(msg)
258
+ raise BookalimoError(msg) from e
259
+ except gexc.GoogleAPICallError as e:
260
+ msg = f"{ctx} failed: {_fmt_exc(e)}"
261
+ logger.error(msg)
262
+ raise BookalimoError(msg) from e
263
+
264
+
265
+ async def _call_gplaces_async(
266
+ ctx: str,
267
+ fn: Callable[..., Awaitable[Any]],
268
+ *args: Any,
269
+ not_found_ok: bool = False,
270
+ **kwargs: Any,
271
+ ) -> Any:
272
+ try:
273
+ return await fn(*args, **kwargs)
274
+ except gexc.NotFound:
275
+ if not_found_ok:
276
+ return None
277
+ raise BookalimoError(f"{ctx} not found") from None
278
+ except gexc.InvalidArgument as e:
279
+ msg = f"{ctx} invalid argument: {_fmt_exc(e)}"
280
+ logger.error(msg)
281
+ raise BookalimoError(msg) from e
282
+ except gexc.GoogleAPICallError as e:
283
+ msg = f"{ctx} failed: {_fmt_exc(e)}"
284
+ logger.error(msg)
285
+ raise BookalimoError(msg) from e
286
+
287
+
288
+ def handle_autocomplete_impl(
289
+ transport_call: Callable[[dict[str, Any]], Any],
290
+ request: models.AutocompletePlacesRequest,
291
+ ) -> models.AutocompletePlacesResponse:
292
+ """Shared implementation for autocomplete method."""
293
+
294
+ proto = _call_gplaces_sync(
295
+ "Google Places Autocomplete", transport_call, request.model_dump()
296
+ )
297
+ return validate_proto_to_model(proto, models.AutocompletePlacesResponse)
298
+
299
+
300
+ def prepare_geocode_params(
301
+ request: models.GeocodingRequest,
302
+ api_key: str,
303
+ ) -> Any:
304
+ """Prepare geocode parameters for both sync/async."""
305
+ params = request.to_query_params()
306
+ params = params.add("key", api_key)
307
+ return params
308
+
309
+
310
+ def handle_geocode_response(response: Any) -> dict[str, Any]:
311
+ """Handle geocode response for both sync/async."""
312
+ try:
313
+ response.raise_for_status()
314
+ return cast(dict[str, Any], response.json())
315
+ except httpx.HTTPError as e:
316
+ msg = f"HTTP geocoding failed: {_fmt_exc(e)}"
317
+ logger.error(msg)
318
+ raise BookalimoError(msg) from e
319
+
320
+
321
+ async def handle_autocomplete_impl_async(
322
+ transport_call: Callable[[dict[str, Any]], Awaitable[Any]],
323
+ request: models.AutocompletePlacesRequest,
324
+ ) -> models.AutocompletePlacesResponse:
325
+ """Shared implementation for async autocomplete method."""
326
+
327
+ proto = await _call_gplaces_async(
328
+ "Google Places Autocomplete", transport_call, request.model_dump()
329
+ )
330
+ return validate_proto_to_model(proto, models.AutocompletePlacesResponse)
331
+
332
+
333
+ def handle_search_impl(
334
+ transport_call: Callable[[dict[str, Any], tuple[tuple[str, str], ...]], Any],
335
+ query: Optional[str] = None,
336
+ request: Optional[models.SearchTextRequest] = None,
337
+ fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
338
+ **kwargs: Any,
339
+ ) -> list[models.Place]:
340
+ """Shared implementation for search method."""
341
+ request_params, metadata = _prepare_search_params_and_metadata(
342
+ query, request, fields, **kwargs
343
+ )
344
+
345
+ protos = _call_gplaces_sync(
346
+ "Google Places Text Search", transport_call, request_params, metadata
347
+ )
348
+ return _normalize_search_results(protos)
349
+
350
+
351
+ async def handle_search_impl_async(
352
+ transport_call: Callable[
353
+ [dict[str, Any], tuple[tuple[str, str], ...]], Awaitable[Any]
354
+ ],
355
+ query: Optional[str] = None,
356
+ request: Optional[models.SearchTextRequest] = None,
357
+ fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
358
+ **kwargs: Any,
359
+ ) -> list[models.Place]:
360
+ """Shared implementation for async search method."""
361
+ request_params, metadata = _prepare_search_params_and_metadata(
362
+ query, request, fields, **kwargs
363
+ )
364
+
365
+ protos = await _call_gplaces_async(
366
+ "Google Places Text Search", transport_call, request_params, metadata
367
+ )
368
+ return _normalize_search_results(protos)
369
+
370
+
371
+ def handle_get_place_impl(
372
+ transport_call: Callable[[dict[str, Any], tuple[tuple[str, str], ...]], Any],
373
+ place_id: Optional[str] = None,
374
+ request: Optional[models.GetPlaceRequest] = None,
375
+ fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
376
+ ) -> Optional[models.Place]:
377
+ """Shared implementation for get place method."""
378
+ request_params, metadata = _prepare_get_params_and_metadata(
379
+ place_id, request, fields
380
+ )
381
+
382
+ proto = _call_gplaces_sync(
383
+ "Google Places Get Place",
384
+ transport_call,
385
+ request_params,
386
+ metadata,
387
+ not_found_ok=True,
388
+ )
389
+ return None if proto is None else _normalize_place_from_proto(proto)
390
+
391
+
392
+ async def handle_get_place_impl_async(
393
+ transport_call: Callable[
394
+ [dict[str, Any], tuple[tuple[str, str], ...]], Awaitable[Any]
395
+ ],
396
+ place_id: Optional[str] = None,
397
+ request: Optional[models.GetPlaceRequest] = None,
398
+ fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
399
+ ) -> Optional[models.Place]:
400
+ """Shared implementation for async get place method."""
401
+ request_params, metadata = _prepare_get_params_and_metadata(
402
+ place_id, request, fields
403
+ )
404
+
405
+ proto = await _call_gplaces_async(
406
+ "Google Places Get Place",
407
+ transport_call,
408
+ request_params,
409
+ metadata,
410
+ not_found_ok=True,
411
+ )
412
+ return None if proto is None else _normalize_place_from_proto(proto)
413
+
414
+
415
+ def _prepare_search_params_and_metadata(
416
+ query: Optional[str],
417
+ request: Optional[models.SearchTextRequest],
418
+ fields: FieldMaskInput,
419
+ **kwargs: Any,
420
+ ) -> tuple[dict[str, Any], tuple[tuple[str, str], ...]]:
421
+ """Prepare search parameters and metadata for both sync/async."""
422
+ request_params = _build_search_request_params(query, request, **kwargs)
423
+ metadata = _mask_header(fields, prefix="places")
424
+ return request_params, metadata
425
+
426
+
427
+ def _prepare_get_params_and_metadata(
428
+ place_id: Optional[str],
429
+ request: Optional[models.GetPlaceRequest],
430
+ fields: FieldMaskInput,
431
+ ) -> tuple[dict[str, Any], tuple[tuple[str, str], ...]]:
432
+ """Prepare get place parameters and metadata for both sync/async."""
433
+ request_params = _build_get_place_request(place_id, request)
434
+ metadata = _mask_header(fields)
435
+ return request_params, metadata
436
+
437
+
438
+ def create_search_text_request(
439
+ query: str,
440
+ region_code: Optional[str],
441
+ ) -> models.SearchTextRequest:
442
+ return models.SearchTextRequest(
443
+ text_query=query,
444
+ region_code=region_code,
445
+ max_result_count=5,
446
+ rank_preference=RankPreference.RELEVANCE,
447
+ # included_type="airport",
448
+ strict_type_filtering=False,
449
+ )
450
+
451
+
452
+ def handle_resolve_airport_preprocessing(
453
+ query: Optional[str],
454
+ place_id: Optional[str],
455
+ places: Optional[list[models.Place]],
456
+ max_distance_km: Optional[float],
457
+ ) -> tuple[Optional[str], list[models.Place], bool]:
458
+ """
459
+ Handle the preprocessing logic for resolve_airport that doesn't depend on sync/async.
460
+ Returns (effective_query_or_none, places_to_resolve, needs_search_call).
461
+ If needs_search_call is True, the caller should perform a search using the query.
462
+ """
463
+ # Validate inputs
464
+ _validate_resolve_airport_inputs(place_id, places, max_distance_km)
465
+
466
+ # Handle different input scenarios
467
+ if place_id is not None:
468
+ # Caller needs to fetch the place using get()
469
+ return query, [], True # Signal that caller needs to call get()
470
+
471
+ elif places is not None:
472
+ if len(places) == 0 and (query is None or not str(query).strip()):
473
+ raise ValueError(
474
+ "Empty 'places' and no 'query' provided; nothing to resolve."
475
+ )
476
+ return query, places, False
477
+
478
+ else:
479
+ # Neither place_id nor places: fall back to query-driven search
480
+ if query is None or not str(query).strip():
481
+ raise ValueError("Either place_id, places, or query must be provided.")
482
+ return query, [], True # Signal that caller needs to call search()
483
+
484
+
485
+ def handle_resolve_airport_postprocessing(
486
+ query: Optional[str],
487
+ effective_places: list[models.Place],
488
+ max_distance_km: Optional[float],
489
+ max_results: Optional[int],
490
+ confidence_threshold: Optional[float],
491
+ text_weight: float,
492
+ ) -> list[models.ResolvedAirport]:
493
+ """Handle the final processing logic for resolve_airport."""
494
+
495
+ # Derive effective query
496
+ effective_query = _derive_effective_query(query, effective_places)
497
+
498
+ google_places = [
499
+ p.google_place for p in effective_places if p.google_place is not None
500
+ ]
501
+
502
+ return resolve_airport(
503
+ effective_query,
504
+ google_places,
505
+ max_distance_km,
506
+ max_results,
507
+ confidence_threshold,
508
+ text_weight,
509
+ )