google_maps 3.9.5

An unofficial Google Maps Platform client library for the Rust programming language.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
#![allow(clippy::ref_option, reason = "for the getset crate")]

use crate::places_new::FieldMask;
use crate::places_new::nearby_search::Response;
use crate::places_new::types::request::{LocationRestriction, PlaceTypeSet, RankPreference};
use icu_locale::Locale;
use reqwest::header::HeaderMap;
use rust_iso3166::CountryCode;

// -------------------------------------------------------------------------------------------------
//
/// Request for the Google Maps Places API (New) Nearby Search service.
///
/// Used to search for places within a specified geographic area, optionally filtered by place
/// type.
///
/// Unlike Text Search which uses text queries, Nearby Search focuses purely on location and
/// type-based discovery. Results can be ranked by distance or popularity. At minimum, you must
/// provide a location restriction and field mask to make a valid request.
///
/// # Understanding Type Filtering
///
/// Nearby Search offers two ways to filter by place type:
///
/// * **`included_types` / `excluded_types`** - Filter on ANY type in a place's type list (broad
///   search). A hotel with a restaurant inside would match `included_types: ["restaurant"]`.
///
/// * **`included_primary_types` / `excluded_primary_types`** - Filter on what the business
///   PRIMARILY is (precise search). That same hotel would NOT match `included_primary_types:
///   ["restaurant"]` because it's primarily a "lodging" business.
///
/// Use primary type filtering when you want businesses where this IS the main category, not just
/// an amenity.
///
/// # `Request` vs. `RequestWithClient`
///
/// * `Request` - Serializable, no client reference. For caching, storage, transmission.
/// * `RequestWithClient` - Contains client reference, executable. For immediate use.
///
/// You can convert between these types using `with_client()` or `into()`.
#[derive(
    //std
    Clone,
    Debug,
    // serde
    serde::Serialize,
    // getset
    getset::Getters,
    getset::CopyGetters,
    getset::MutGetters,
    getset::Setters,
    // other
    bon::Builder
)]
#[serde(rename_all = "camelCase")]
pub struct RequestWithClient<'c> {
    /// The Google Maps API client.
    ///
    /// The `Client` structure contains the application's API key and other user-definable settings
    /// such as "maximum retries," and most importantly the
    /// [reqwest](https://crates.io/crates/reqwest) client itself.
    #[serde(skip_deserializing, skip_serializing)]
    pub(crate) client: &'c crate::Client,

    /// Fields to include in the response.
    ///
    /// Specifies which place data to return. This directly impacts API costs since different fields
    /// trigger different SKU charges. Use specific fields rather than `FieldMask::all()` to
    /// optimize costs.
    ///
    /// Field masking is a good design practice to ensure that you don't request unnecessary data,
    /// which helps to avoid unnecessary processing time and billing charges.
    ///
    /// While the `FieldMask::all()` is fine to use in development, Google discourages the use of
    /// the wildcard response field mask in production because of the large amount of data that can
    /// be returned.
    ///
    /// > ℹ️ Further guidance for using `places.iconMaskBaseUri` and `places.iconBackgroundColor`
    /// > can be found in [Place
    /// > Icons](https://developers.google.com/maps/documentation/places/web-service/icons) section.
    #[serde(skip)]
    #[builder(into)]
    #[getset(get = "pub", set = "pub", get_mut = "pub")]
    pub field_mask: FieldMask,

    /// Restrict search results to a specified geographic region.
    ///
    /// Location restriction hard-limits results to only include places within the specified area,
    /// completely excluding results outside that area.
    ///
    /// Use this when you need strict geographic boundaries, such as limiting searches to a specific
    /// city or service area.
    #[builder(into)]
    #[getset(get = "pub", set = "pub", get_mut = "pub")]
    pub location_restriction: LocationRestriction,

    /// Place types to include in results from
    /// [Table A](https://developers.google.com/maps/documentation/places/web-service/place-types#table-a).
    ///
    /// Matches places that have any of the specified types **anywhere in their type list**. This
    /// casts a wide net since each place can have multiple types. Up to 50 types can be specified.
    ///
    /// Every place in Google's database has multiple types - for example, a seafood restaurant
    /// might have types `["seafood_restaurant", "restaurant", "food", "point_of_interest",
    /// "establishment"]`. This filter matches if **any** of those types appear in your list.
    ///
    /// # Example
    ///
    /// Searching with `included_types: ["restaurant"]` will match:
    /// - A seafood restaurant (has "restaurant" in its types)
    /// - A hotel with a restaurant inside (has "restaurant" in its types)
    /// - Any place tagged with "restaurant" regardless of what it primarily is
    ///
    /// If this parameter is omitted, places of all types are returned.
    ///
    /// > ℹ️ If both `included_types` and `included_primary_types` are set, results must satisfy at
    /// > least one condition from each list.
    #[serde(default, skip_serializing_if = "PlaceTypeSet::is_empty")]
    #[builder(default, into)]
    #[getset(get = "pub", set = "pub", get_mut = "pub")]
    pub included_types: PlaceTypeSet,

    /// Place types to exclude from results from
    /// [Table A](https://developers.google.com/maps/documentation/places/web-service/place-types#table-a).
    ///
    /// Filters out places that have any of the specified types **anywhere in their type list**. Up
    /// to 50 types can be specified. A place is excluded if it has any type in this list.
    ///
    /// # Example
    ///
    /// `{"includedTypes": ["school"], "excludedTypes": ["primary_school"]}` returns places with
    /// "school" in their types but excludes any place that has "`primary_school`" in their types.
    ///
    /// > ⚠️ If a type appears in both `included_types` and `excluded_types`, the API returns an
    /// > `INVALID_REQUEST` error.
    #[serde(default, skip_serializing_if = "PlaceTypeSet::is_empty")]
    #[builder(default, into)]
    #[getset(get = "pub", set = "pub", get_mut = "pub")]
    pub excluded_types: PlaceTypeSet,

    /// Primary place types to include in results from
    /// [Table A](https://developers.google.com/maps/documentation/places/web-service/place-types#table-a).
    ///
    /// Matches places where **the business's primary category** is one of the specified types.
    /// This is a more precise filter than `included_types` since each place has exactly one
    /// primary type that represents what the business mainly is. Up to 50 types can be specified.
    ///
    /// Every place in Google's database is classified with one primary type. For example, a
    /// seafood restaurant has `primary_type: "seafood_restaurant"` even though it also has types
    /// like `["seafood_restaurant", "restaurant", "food", ...]`. A hotel with a restaurant inside
    /// has `primary_type: "lodging"` even though "restaurant" appears in its types list.
    ///
    /// # Example
    ///
    /// Searching with `included_primary_types: ["restaurant"]` will match:
    /// - Places primarily categorized as "restaurant"
    /// - May also match specialized subtypes like "`chinese_restaurant`" (Google treats general
    ///   types as matching their specializations)
    ///
    /// But will NOT match:
    /// - A hotel that happens to have a restaurant inside (its primary type is "lodging")
    ///
    /// # When to Use
    ///
    /// Use `included_primary_types` when you want places where this IS the main business, not just
    /// an amenity or secondary feature.
    ///
    /// > ℹ️ If both `included_types` and `included_primary_types` are set, results must satisfy at
    /// > least one condition from each list.
    #[serde(default, skip_serializing_if = "PlaceTypeSet::is_empty")]
    #[builder(default, into)]
    #[getset(get = "pub", set = "pub", get_mut = "pub")]
    pub included_primary_types: PlaceTypeSet,

    /// Primary place types to exclude from results from
    /// [Table A](https://developers.google.com/maps/documentation/places/web-service/place-types#table-a).
    ///
    /// Filters out places where **the business's primary category** is one of the specified types.
    /// Up to 50 types can be specified.
    ///
    /// # Example
    ///
    /// `{"includedTypes": ["restaurant"], "excludedPrimaryTypes": ["steak_house"]}` returns places
    /// that offer restaurant services (has "restaurant" somewhere in their types list) but
    /// excludes places whose main business is a steak house (primary type is "`steak_house`").
    ///
    /// This would include:
    /// - Seafood restaurants that serve some steak dishes (primary type: "`seafood_restaurant`")
    /// - Italian restaurants (primary type: "`italian_restaurant`")
    ///
    /// But exclude:
    /// - Steakhouses (primary type: "`steak_house`")
    ///
    /// > ⚠️ If a type appears in both `included_primary_types` and `excluded_primary_types`, the
    /// > API returns an `INVALID_ARGUMENT` error.
    #[serde(default, skip_serializing_if = "PlaceTypeSet::is_empty")]
    #[builder(default, into)]
    #[getset(get = "pub", set = "pub", get_mut = "pub")]
    pub excluded_primary_types: PlaceTypeSet,

    /// Language for results.
    ///
    /// Uses a BCP-47 language code. Defaults to `en` if not specified. Street addresses appear in
    /// the local language (transliterated if needed), while other text uses this preferred language
    /// when available.
    ///
    /// # Notes
    ///
    /// * See the [list of supported languages](https://developers.google.com/maps/faq#languagesupport).
    ///   Google often updates the supported languages, so this list may not be exhaustive.
    ///
    /// * If `language` is not supplied, the API defaults to `en`. If you specify an invalid
    ///   language code, the API returns an `INVALID_ARGUMENT` error.
    ///
    /// * The API does its best to provide a street address that is readable for both the user and
    ///   locals. To achieve that goal, it returns street addresses in the local language,
    ///   transliterated to a script readable by the user if necessary, observing the preferred
    ///   language. All other addresses are returned in the preferred language. Address components
    ///   are all returned in the same language, which is chosen from the first component.
    ///
    /// * If a name is not available in the preferred language, the API uses the closest match.
    ///
    /// * The preferred language has a small influence on the set of results that the API chooses to
    ///   return, and the order in which they are returned. The geocoder interprets abbreviations
    ///   differently depending on language, such as the abbreviations for street types, or synonyms
    ///   that may be valid in one language but not in another.
    #[serde(
        rename = "languageCode",
        default,
        skip_serializing_if = "Option::is_none",
        serialize_with = "crate::places_new::serde::serialize_optional_locale",
        deserialize_with = "crate::places_new::serde::deserialize_optional_locale"
    )]
    #[builder(into)]
    #[getset(get = "pub", set = "pub", get_mut = "pub")]
    pub language: Option<Locale>,

    /// Maximum number of place results to return (1-20).
    ///
    /// Specifies the maximum number of places to return in the response. Must be between 1 and 20
    /// inclusive, with a default of 20 if not specified.
    ///
    /// > ℹ️ Unlike Text Search, Nearby Search does not support pagination. The maximum 20 results
    /// > is a hard limit per request.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    #[getset(get = "pub", set = "pub", get_mut = "pub")]
    pub max_result_count: Option<i32>,

    /// How to rank results in the response.
    ///
    /// Specifies the ranking method for results:
    ///
    /// * `Popularity` (default) - Ranks results based on their popularity.
    ///
    /// * `Distance` - Ranks results in ascending order by their distance from the specified
    ///   location.
    ///
    /// If this parameter is omitted, results are ranked by popularity.
    ///
    /// > ℹ️ This parameter does not apply to hotel queries or geopolitical queries (for example,
    /// > queries related to administrative areas, localities, postal codes, school districts, or
    /// > countries).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    #[getset(get = "pub", set = "pub", get_mut = "pub")]
    pub rank_preference: Option<RankPreference>,

    /// Region for formatting the response.
    ///
    /// Affects how addresses are formatted (e.g., country code omitted if it matches) and can bias
    /// results based on applicable law.
    ///
    /// The region code used to format the response, specified as a [two-character CLDR
    /// code](https://www.unicode.org/cldr/charts/latest/supplemental/territory_language_information.html)
    /// value. This parameter can also have a bias effect on the search results. There is no default
    /// value.
    ///
    /// If the country name of the `formatted_address` field in the response matches the `region`,
    /// the country code is omitted from `formatted_address`. This parameter has no effect on
    /// `adr_format_address`, which always includes the country name when available, or on
    /// `short_formatted_address`, which never includes it.
    ///
    /// Most CLDR codes are identical to ISO 3166-1 codes, with some notable exceptions. For
    /// example, the United Kingdom's ccTLD is "uk" (.co.uk) while its ISO 3166-1 code is "gb"
    /// (technically for the entity of "The United Kingdom of Great Britain and Northern Ireland").
    /// The parameter can affect results based on applicable law.
    #[serde(
        rename = "regionCode",
        default,
        skip_serializing_if = "Option::is_none",
        serialize_with = "crate::places_new::serde::serialize_optional_country_code",
        deserialize_with = "crate::places_new::serde::deserialize_optional_country_code"
    )]
    pub region: Option<CountryCode>,
}

// -------------------------------------------------------------------------------------------------
//
// Method Implementations

impl RequestWithClient<'_> {
    /// Converts to a serializable request by stripping the client reference.
    ///
    /// Creates a `Request` containing all query parameters but without the client reference, making
    /// it possible to serialize, store, or transmit. Use this when you need to persist request
    /// state for later execution.
    ///
    /// Note: Typically you don't need to call this directly, as `execute()` automatically returns a
    /// `Response` containing the serializable request.
    #[must_use]
    pub fn into_request(self) -> crate::places_new::nearby_search::Request {
        self.into()
    }

    /// Executes the text search request.
    ///
    /// Sends the configured request to the Google Maps API and returns both the response and a
    /// serializable copy of the request parameters in a `Response`.
    ///
    /// The returned context preserves all request state including the pagination token, enabling
    /// pagination continuation with `Client::next_nearby_search()`.
    ///
    /// This method is available on both the builder (via `.build().execute()` shorthand) and on
    /// `RequestWithClient` directly when constructing requests manually.
    pub async fn execute(self) -> Result<Response, crate::Error> {
        let response = self.client.post_request(&self).await?;
        Ok(response)
    }
}

impl<S: request_with_client_builder::State> RequestWithClientBuilder<'_, S> {
    /// Executes the text search request.
    ///
    /// Builds the request and sends it to the Google Maps API, returning the parsed text search
    /// response. This method both completes the builder and executes the HTTP request in one step.
    pub async fn execute(self) -> Result<Response, crate::Error>
    where
        S: request_with_client_builder::IsComplete,
    {
        let request = self.build();  // Build request
        let response = request.client.post_request(&request).await?;
        Ok(response)
    }
}

impl crate::client::Client {
    /// Searches for places within a geographic area.
    ///
    /// The Google Maps Places API Nearby Search (New) service finds places within a specified
    /// circular area, optionally filtered by place type.
    ///
    /// Unlike Text Search which uses text queries, Nearby Search focuses purely on location-based
    /// discovery - perfect for "what's nearby" functionality or finding specific types of places
    /// within a radius.
    ///
    /// Results can be ranked by distance (closest first) or popularity (most relevant first).
    /// This endpoint does not support pagination - the maximum 20 results per request is a hard
    /// limit.
    ///
    /// Use field masking to control which place data is returned and manage API costs, as you're
    /// charged based on the fields requested. Different field groups trigger different pricing
    /// SKUs (Pro, Enterprise, Enterprise + Atmosphere).
    ///
    /// # Location Restriction
    ///
    /// The location must be specified as a circle (center point + radius). You can provide this
    /// as:
    /// - A 3-tuple: `(latitude, longitude, radius_meters)`
    /// - A `Circle` struct
    /// - A `LocationRestriction::Circle` enum variant
    ///
    /// **Note**: Nearby Search only supports circular search areas. Rectangle/viewport
    /// restrictions will fail validation.
    ///
    /// # Examples
    ///
    /// Basic nearby search using tuple syntax:
    ///
    /// ```rust,no_run
    /// use google_maps::places_new::{Field, PlaceType};
    ///
    /// // Find restaurants within 5km of San Francisco
    /// let response = google_maps_client
    ///     .nearby_search((37.7749, -122.4194, 5000.0))?
    ///     .field_mask([
    ///         Field::PlacesDisplayName,
    ///         Field::PlacesFormattedAddress,
    ///         Field::PlacesRating,
    ///     ])
    ///     .included_primary_types(vec![PlaceType::Restaurant])
    ///     .execute()
    ///     .await?;
    ///
    /// for place in &response {
    ///     if let Some(name) = place.display_text() {
    ///         println!("Found: {}", name);
    ///     }
    /// }
    /// ```
    ///
    /// Rank by distance to find the closest places:
    ///
    /// ```rust,no_run
    /// use google_maps::places_new::{Field, RankPreference};
    ///
    /// let response = google_maps_client
    ///     .nearby_search((40.7128, -74.0060, 2000.0))?
    ///     .field_mask([Field::PlacesDisplayName, Field::PlacesLocation])
    ///     .rank_preference(RankPreference::Distance)
    ///     .execute()
    ///     .await?;
    ///
    /// println!("Closest {} places:", response.len());
    /// ```
    ///
    /// Filter by multiple types and exclude specific types:
    ///
    /// ```rust,no_run
    /// use google_maps::places_new::{Field, PlaceType};
    ///
    /// // Find schools but exclude primary schools
    /// let response = google_maps_client
    ///     .nearby_search((35.6762, 139.6503, 3000.0))?
    ///     .field_mask([Field::PlacesDisplayName, Field::PlacesPrimaryType])
    ///     .included_types(vec![PlaceType::School])
    ///     .excluded_types(vec![PlaceType::PrimarySchool])
    ///     .max_result_count(10)
    ///     .execute()
    ///     .await?;
    /// ```
    ///
    /// Using an explicit Circle for more control:
    ///
    /// ```rust,no_run
    /// use google_maps::places_new::{Circle, LatLng, Field};
    /// use rust_decimal_macros::dec;
    ///
    /// let center = LatLng::try_from_dec(dec!(51.5074), dec!(-0.1278))?;
    /// let circle = Circle::try_new(center, dec!(1000.0))?;
    ///
    /// let response = google_maps_client
    ///     .nearby_search(circle)?
    ///     .field_mask([Field::PlacesDisplayName])
    ///     .execute()
    ///     .await?;
    /// ```
    pub fn nearby_search<L>(
        &self,
        location_restriction: L,
    ) -> Result<
        RequestWithClientBuilder<
            '_,
            crate::places_new::nearby_search::request_with_client::request_with_client_builder::SetLocationRestriction<
                crate::places_new::nearby_search::request_with_client::request_with_client_builder::SetClient
            >
        >,
        crate::Error,
    >
    where
        L: TryInto<LocationRestriction>,
        L::Error: Into<crate::Error>,
    {
        let location_restriction = location_restriction
            .try_into()
            .map_err(Into::into)?;

        Ok(RequestWithClient::builder()
            .client(self)
            .location_restriction(location_restriction))
    }
}

// -------------------------------------------------------------------------------------------------
//
// Trait Implementations

#[cfg(feature = "reqwest")]
use crate::request_rate::api::Api;

/// Defines the Google Maps Places API HTTP endpoint for requests.
///
/// This trait returns information needed to make HTTP `POST` requests to the Places API endpoint.
/// It includes service URL, debugging info, and rate-limiting configuration.
impl crate::traits::EndPoint for &RequestWithClient<'_> {
    fn service_url() -> &'static str {
        "https://places.googleapis.com/v1/places:searchNearby"
    }

    fn output_format() -> std::option::Option<&'static str> {
        None // No need to specify the output format, this end-point always returns JSON.
    }

    #[cfg(feature = "reqwest")]
    fn title() -> &'static str {
        "Places API (New) Nearby Search"
    }

    #[cfg(feature = "reqwest")]
    fn apis() -> &'static [Api] {
        &[Api::All, Api::PlacesNew, Api::NearbySearch]
    }
}

#[cfg(feature = "reqwest")]
impl crate::traits::RequestBody for &RequestWithClient<'_> {
    /// Converts the `RequestWithClient` struct into JSON for submission to Google Maps.
    ///
    /// Serializes the request body fields into a JSON object for the HTTP POST request body.
    ///
    /// # Errors
    ///
    /// Returns an error if JSON serialization fails.
    fn request_body(&self) -> Result<String, crate::Error> {
        Ok(serde_json::to_string(self)?)
    }
}

#[cfg(feature = "reqwest")]
impl crate::traits::QueryString for &RequestWithClient<'_> {
    /// Builds the URL query string for the HTTP request.
    ///
    /// The Places (New) API uses the HTTP body for most request data, so the query string only
    /// contains the API key for authentication.
    ///
    /// ## Arguments
    ///
    /// This method accepts no arguments.
    fn query_string(&self) -> String {
        String::new()
    }
}

#[cfg(feature = "reqwest")]
impl crate::traits::RequestHeaders for &RequestWithClient<'_> {
    /// Returns a map of HTTP header names to values.
    ///
    /// These headers will be added to the HTTP request alongside the standard headers like
    /// `X-Goog-Api-Key`.
    fn request_headers(&self) -> HeaderMap {
        let field_mask = self.field_mask().to_string();
        let mut headers = HeaderMap::new();
        match reqwest::header::HeaderValue::from_str(field_mask.as_str()) {
            Ok(header_value) => { headers.insert("X-Goog-FieldMask", header_value); },
            Err(error) => tracing::error!("error building request headers: {error}"),
        }
        headers
    }

    /// Returns whether the `X-Goog-Api-Key` header should be set for this request.
    fn send_x_goog_api_key() -> bool {
        true
    }
}

#[cfg(feature = "reqwest")]
impl crate::traits::Validatable for &RequestWithClient<'_> {
    /// Validates the nearby search request parameters.
    ///
    /// Checks that the combination of parameters makes sense and will be accepted by the Google
    /// Maps Places API. This does not validate individual parameter values (like coordinate
    /// ranges), only the logical consistency of the request.
    ///
    /// ## Validation Rules
    ///
    /// - `field_mask` cannot be empty when using `FieldMask::Specific`
    /// - All place types in `included_types`, `excluded_types`, `included_primary_types`, and
    ///   `excluded_primary_types` must be Table A types (Table B types cannot be used for
    ///   filtering)
    /// - A type cannot appear in both `included_types` and `excluded_types`
    /// - A type cannot appear in both `included_primary_types` and `excluded_primary_types`
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Field mask is `FieldMask::Specific` with an empty vector
    /// - Any place type in the filter lists is a Table B place type
    /// - A place type appears in both inclusion and exclusion lists
    /// - Location restriction is a Rectangle (only Circle is supported)
    fn validate(&self) -> Result<(), crate::Error> {
        // Check that field mask is not empty
        if let crate::places_new::FieldMask::Specific(fields) = &self.field_mask {
            if fields.is_empty() {
                let debug = "field_mask: FieldMask::Specific(vec![])".to_string();
                let span = (0, debug.len());
                return Err(crate::places_new::nearby_search::Error::EmptyFieldMask {
                    debug,
                    span: span.into(),
                }
                .into());
            }
        }

        // Helper to validate that all types in a vec are Table A
        let validate_table_a = |types: &PlaceTypeSet, field_name: &str| {
            for place_type in types {
                if place_type.is_table_b() {
                    let debug = format!("{field_name}: vec![..., PlaceType::{place_type}, ...]");
                    let span = (0, debug.len());
                    return Err::<(), crate::Error>(crate::places_new::nearby_search::Error::InvalidPlaceTypeForFilter {
                        place_type: place_type.to_string(),
                        debug,
                        span: span.into(),
                    }
                    .into());
                }
            }
            Ok(())
        };

        // Nearby Search only supports Circle, not Rectangle
        if let LocationRestriction::Rectangle(viewport) = &self.location_restriction {
            let debug = format!(
                "location_restriction: Rectangle(Viewport {{ low: {}, high: {} }})",
                viewport.low, viewport.high
            );
            let span = (0, debug.len());

            return Err(crate::places_new::nearby_search::Error::UnsupportedLocationRestriction {
                restriction_type: "Rectangle".to_string(),
                debug,
                span: span.into(),
            }.into());
        }

        // Validate all type filter fields contain only Table A types
        validate_table_a(&self.included_types, "included_types")?;
        validate_table_a(&self.excluded_types, "excluded_types")?;
        validate_table_a(&self.included_primary_types, "included_primary_types")?;
        validate_table_a(&self.excluded_primary_types, "excluded_primary_types")?;

        // Check for conflicts between included_types and excluded_types
        for place_type in &self.included_types {
            if self.excluded_types.contains(place_type) {
                let debug = format!(
                    "included_types: vec![..., PlaceType::{place_type}, ...], \
                     excluded_types: vec![..., PlaceType::{place_type}, ...]"
                );
                let span = (0, debug.len());
                return Err(
                    crate::places_new::nearby_search::Error::ConflictingPlaceTypes {
                        place_type: place_type.to_string(),
                        debug,
                        span: span.into(),
                    }
                    .into(),
                );
            }
        }

        // Check for conflicts between included_primary_types and excluded_primary_types
        for place_type in &self.included_primary_types {
            if self.excluded_primary_types.contains(place_type) {
                let debug = format!(
                    "included_primary_types: vec![..., PlaceType::{place_type}, ...], \
                     excluded_primary_types: vec![..., PlaceType::{place_type}, ...]"
                );
                let span = (0, debug.len());
                return Err(
                    crate::places_new::nearby_search::Error::ConflictingPrimaryPlaceTypes {
                        place_type: place_type.to_string(),
                        debug,
                        span: span.into(),
                    }
                    .into(),
                );
            }
        }

        Ok(())
    }
}