everymap_providers_google/domain/search/
mod.rs1pub 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
14pub 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
102fn 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(¶ms);
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 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 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(¶ms);
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}