Skip to main content

everymap_providers_google/domain/search/
mod.rs

1pub mod types;
2
3use crate::client::GoogleClient;
4use async_trait::async_trait;
5use everymap_core::domains::search::{
6    GeocodeOptions, Geocoder, ReverseGeocodeOptions, SearchResponse, SearchResult, SearchResultType,
7};
8use everymap_core::error::{EveryMapError, EveryMapResult};
9use everymap_core::types::{Address, BoundingBox, Coordinate};
10pub use types::*;
11
12const GEOCODING_BASE_URL: &str = "https://maps.googleapis.com/maps/api/geocode/json";
13
14/// Implementation of `Geocoder` for Google Maps Geocoding API.
15pub struct GoogleGeocoder {
16    pub(crate) client: std::sync::Arc<GoogleClient>,
17    pub(crate) base_url: String,
18}
19
20impl GoogleGeocoder {
21    pub fn new(client: std::sync::Arc<GoogleClient>) -> Self {
22        Self {
23            client,
24            base_url: GEOCODING_BASE_URL.to_string(),
25        }
26    }
27
28    pub fn with_base_url(client: std::sync::Arc<GoogleClient>, base_url: String) -> Self {
29        Self { client, base_url }
30    }
31}
32
33impl From<GoogleGeocodeResult> for SearchResult {
34    fn from(result: GoogleGeocodeResult) -> Self {
35        let coordinate = result
36            .geometry
37            .as_ref()
38            .and_then(|g| g.location.as_ref())
39            .map(|loc| Coordinate::new(loc.lat, loc.lng).unwrap_or(Coordinate::ORIGIN))
40            .unwrap_or(Coordinate::ORIGIN);
41
42        let bounding_box = result
43            .geometry
44            .as_ref()
45            .and_then(|g| g.viewport.as_ref())
46            .map(|v| {
47                BoundingBox::new(
48                    Coordinate::new(v.northeast.lat, v.northeast.lng).unwrap_or(Coordinate::ORIGIN),
49                    Coordinate::new(v.southwest.lat, v.southwest.lng).unwrap_or(Coordinate::ORIGIN),
50                )
51            });
52
53        let address = Address {
54            label: result.formatted_address.clone(),
55            street: extract_component(&result.address_components, "street_number", "route"),
56            city: extract_component_long(&result.address_components, "locality").or_else(|| {
57                extract_component_long(&result.address_components, "administrative_area_level_2")
58            }),
59            state: extract_component_long(
60                &result.address_components,
61                "administrative_area_level_1",
62            ),
63            country_code: extract_component_short(&result.address_components, "country"),
64            postal_code: extract_component_long(&result.address_components, "postal_code"),
65            district: extract_component_long(&result.address_components, "sublocality"),
66            house_number: extract_component_long(&result.address_components, "street_number"),
67            ..Address::default()
68        };
69
70        let result_type = classify_result_type(&result.types);
71
72        Self {
73            id: result.place_id.clone(),
74            title: result.formatted_address.clone(),
75            coordinate,
76            address,
77            result_type,
78            distance: None,
79            confidence: None,
80            categories: vec![],
81            bounding_box,
82            raw: Some(serde_json::to_value(result).unwrap_or_default()),
83        }
84    }
85}
86
87fn classify_result_type(types: &[String]) -> SearchResultType {
88    if types
89        .iter()
90        .any(|t| t == "street_address" || t == "premise")
91    {
92        SearchResultType::ExactMatch
93    } else if types.iter().any(|t| t == "route" || t == "intersection") {
94        SearchResultType::Approximate
95    } else if types.iter().any(|t| t == "political" || t == "locality") {
96        SearchResultType::Interpolated
97    } else {
98        SearchResultType::Unknown
99    }
100}
101
102/// Extract a formatted street address from components.
103fn extract_component(
104    components: &[GoogleAddressComponent],
105    number_type: &str,
106    street_type: &str,
107) -> Option<String> {
108    let number = components
109        .iter()
110        .find(|c| c.types.iter().any(|t| t == number_type))
111        .and_then(|c| c.long_name.clone());
112    let street = components
113        .iter()
114        .find(|c| c.types.iter().any(|t| t == street_type))
115        .and_then(|c| c.long_name.clone());
116    match (number, street) {
117        (Some(n), Some(s)) => Some(format!("{} {}", n, s)),
118        (None, Some(s)) => Some(s),
119        (Some(n), None) => Some(n),
120        (None, None) => None,
121    }
122}
123
124fn extract_component_long(
125    components: &[GoogleAddressComponent],
126    component_type: &str,
127) -> Option<String> {
128    components
129        .iter()
130        .find(|c| c.types.iter().any(|t| t == component_type))
131        .and_then(|c| c.long_name.clone())
132}
133
134fn extract_component_short(
135    components: &[GoogleAddressComponent],
136    component_type: &str,
137) -> Option<String> {
138    components
139        .iter()
140        .find(|c| c.types.iter().any(|t| t == component_type))
141        .and_then(|c| c.short_name.clone())
142}
143
144#[async_trait]
145impl Geocoder for GoogleGeocoder {
146    async fn geocode(
147        &self,
148        query: &str,
149        options: &GeocodeOptions,
150    ) -> EveryMapResult<SearchResponse> {
151        let mut params: Vec<(&str, String)> = vec![("address", query.to_string())];
152
153        if let Some(lang) = &options.language {
154            params.push(("language", lang.clone()));
155        }
156        if let Some(bbox) = &options.bounding_box {
157            params.push((
158                "bounds",
159                format!(
160                    "{},{}|{},{}",
161                    bbox.south_west.lat,
162                    bbox.south_west.lng,
163                    bbox.north_east.lat,
164                    bbox.north_east.lng
165                ),
166            ));
167        }
168        if !options.country_codes.is_empty() {
169            let components = options
170                .country_codes
171                .iter()
172                .map(|code| format!("country:{}", code.to_lowercase()))
173                .collect::<Vec<_>>()
174                .join("|");
175            params.push(("components", components));
176        }
177        if let Some(extra) = &options.provider_extra {
178            if let Some(obj) = extra.as_object() {
179                if let Some(v) = obj.get("region").and_then(|v| v.as_str()) {
180                    params.push(("region", v.to_string()));
181                }
182                if let Some(v) = obj.get("components").and_then(|v| v.as_str()) {
183                    params.push(("components", v.to_string()));
184                }
185            }
186        }
187
188        let url = self.base_url.clone();
189        let builder = self
190            .client
191            .build_request(reqwest::Method::GET, &url)
192            .query(&params);
193
194        let google_res: GoogleGeocodeResponse = self.client.request_json(builder).await?;
195
196        if google_res.status != "OK" && google_res.status != "ZERO_RESULTS" {
197            return Err(EveryMapError::provider(
198                "google",
199                &google_res.status,
200                google_res
201                    .error_message
202                    .as_deref()
203                    .unwrap_or("Unknown error"),
204            ));
205        }
206
207        let mut items: Vec<SearchResult> = google_res
208            .results
209            .into_iter()
210            .map(SearchResult::from)
211            .collect();
212
213        // Google API lacks a limit parameter; truncate client-side
214        if let Some(limit) = options.limit {
215            items.truncate(limit as usize);
216        }
217
218        Ok(SearchResponse { items })
219    }
220
221    async fn reverse_geocode(
222        &self,
223        coordinate: &Coordinate,
224        options: &ReverseGeocodeOptions,
225    ) -> EveryMapResult<SearchResponse> {
226        let mut params: Vec<(&str, String)> =
227            vec![("latlng", format!("{},{}", coordinate.lat, coordinate.lng))];
228
229        if let Some(lang) = &options.language {
230            params.push(("language", lang.clone()));
231        }
232        if options.limit.is_some() {
233            log::warn!(
234                "Google Reverse Geocoding API does not support a limit parameter; \
235                 limit will be ignored"
236            );
237        }
238        if options.radius.is_some() {
239            log::warn!(
240                "Google Reverse Geocoding API does not support a radius parameter; \
241                 radius will be ignored"
242            );
243        }
244        // Extract Google-specific options from provider_extra
245        if let Some(extra) = &options.provider_extra {
246            if let Some(obj) = extra.as_object() {
247                if let Some(v) = obj.get("result_type").and_then(|v| v.as_str()) {
248                    params.push(("result_type", v.to_string()));
249                }
250                if let Some(v) = obj.get("location_type").and_then(|v| v.as_str()) {
251                    params.push(("location_type", v.to_string()));
252                }
253            }
254        }
255
256        let url = self.base_url.clone();
257        let builder = self
258            .client
259            .build_request(reqwest::Method::GET, &url)
260            .query(&params);
261
262        let google_res: GoogleGeocodeResponse = self.client.request_json(builder).await?;
263
264        if google_res.status != "OK" && google_res.status != "ZERO_RESULTS" {
265            return Err(EveryMapError::provider(
266                "google",
267                &google_res.status,
268                google_res
269                    .error_message
270                    .as_deref()
271                    .unwrap_or("Unknown error"),
272            ));
273        }
274
275        let items: Vec<SearchResult> = google_res
276            .results
277            .into_iter()
278            .map(SearchResult::from)
279            .collect();
280
281        Ok(SearchResponse { items })
282    }
283}