everymap_providers_mapbox/domain/search/
mod.rs1pub mod types;
2
3use crate::client::MapBoxClient;
4use async_trait::async_trait;
5use everymap_core::domains::search::{
6 GeocodeOptions, Geocoder, ReverseGeocodeOptions, SearchResponse, SearchResult, SearchResultType,
7};
8use everymap_core::error::EveryMapResult;
9use everymap_core::types::{Address, BoundingBox, Coordinate};
10use std::sync::Arc;
11
12pub use types::*;
13
14const SEARCH_BASE_URL: &str = "https://api.mapbox.com";
15
16pub struct MapBoxGeocoder {
18 pub(crate) client: Arc<MapBoxClient>,
19 pub(crate) base_url: String,
20}
21
22impl MapBoxGeocoder {
23 pub fn new(client: Arc<MapBoxClient>) -> Self {
24 Self {
25 client,
26 base_url: SEARCH_BASE_URL.to_string(),
27 }
28 }
29
30 pub fn with_base_url(client: Arc<MapBoxClient>, base_url: String) -> Self {
31 Self { client, base_url }
32 }
33}
34
35fn classify_feature_type(feature_type: &str) -> SearchResultType {
37 match feature_type {
38 "address" | "poi" => SearchResultType::ExactMatch,
39 "place" | "locality" | "neighborhood" | "region" | "district" | "country" => {
40 SearchResultType::Approximate
41 }
42 _ => SearchResultType::Unknown,
43 }
44}
45
46impl From<MapBoxFeature> for SearchResult {
47 fn from(f: MapBoxFeature) -> Self {
48 let props = f.properties.as_ref();
49
50 let coordinate = props
52 .and_then(|p| p.coordinates.as_ref())
53 .map(|c| Coordinate::new(c.latitude, c.longitude).unwrap_or(Coordinate::ORIGIN))
54 .or_else(|| {
55 f.geometry.as_ref().and_then(|g| {
57 g.coordinates.as_ref().and_then(|coords| {
58 coords.as_array().and_then(|arr| {
59 if arr.len() >= 2 {
60 Some(
61 Coordinate::new(
62 arr[1].as_f64().unwrap_or(0.0),
63 arr[0].as_f64().unwrap_or(0.0),
64 )
65 .unwrap_or(Coordinate::ORIGIN),
66 )
67 } else {
68 None
69 }
70 })
71 })
72 })
73 })
74 .unwrap_or(Coordinate::ORIGIN);
75
76 let title = props.and_then(|p| p.full_address.clone().or(p.name.clone()));
78
79 let mut address = Address::empty();
81 if let Some(p) = props {
82 address.label = p.full_address.clone();
83 address.street = p.name.clone();
84 if let Some(ctx) = &p.context {
85 if let Some(street) = &ctx.street {
86 address.street = Some(street.name.clone().unwrap_or_default());
87 }
88 if let Some(region) = &ctx.region {
89 address.state = region.name.clone();
90 }
91 if let Some(country) = &ctx.country {
92 address.country = country.name.clone();
93 address.country_code = country.country_code.clone();
94 }
95 if let Some(place) = &ctx.place {
96 address.city = Some(place.name.clone().unwrap_or_default());
97 }
98 if let Some(district) = &ctx.district {
99 address.district = district.name.clone();
100 }
101 if let Some(postcode) = &ctx.postcode {
102 address.postal_code = postcode.name.clone();
103 }
104 }
105 if let Some(house) = &p.address {
106 address.house_number = Some(house.clone());
107 }
108 }
109
110 let bounding_box = props.and_then(|p| p.bbox.as_ref()).and_then(|b| {
112 if b.len() >= 4 {
113 Some(BoundingBox::new(
114 Coordinate::new(b[3], b[2]).ok()?, Coordinate::new(b[1], b[0]).ok()?, ))
117 } else {
118 None
119 }
120 });
121
122 let feature_type_str = props.and_then(|p| p.feature_type.as_deref().map(String::from));
123 let result_type = feature_type_str
124 .as_deref()
125 .map(classify_feature_type)
126 .unwrap_or(SearchResultType::Unknown);
127
128 let categories = props
129 .and_then(|p| {
130 let mut cats = Vec::new();
131 if let Some(ft) = &p.feature_type {
132 cats.push(ft.clone());
133 }
134 if let Some(additional) = &p.additional_feature_types {
135 cats.extend(additional.iter().cloned());
136 }
137 if cats.is_empty() {
138 None
139 } else {
140 Some(cats)
141 }
142 })
143 .unwrap_or_default();
144
145 SearchResult {
146 id: f.id.or(props.and_then(|p| p.mapbox_id.clone())),
147 coordinate,
148 address,
149 title,
150 result_type,
151 distance: None,
152 confidence: props.and_then(|p| p.relevance),
153 categories,
154 bounding_box,
155 raw: None,
156 }
157 }
158}
159
160impl From<MapBoxSearchResponse> for SearchResponse {
161 fn from(response: MapBoxSearchResponse) -> Self {
162 SearchResponse {
163 items: response.features.into_iter().map(|f| f.into()).collect(),
164 }
165 }
166}
167
168#[async_trait]
169impl Geocoder for MapBoxGeocoder {
170 async fn geocode(
171 &self,
172 query: &str,
173 options: &GeocodeOptions,
174 ) -> EveryMapResult<SearchResponse> {
175 let url = format!("{}/search/geocode/v6/forward", self.base_url);
176 let mut params: Vec<(&str, String)> = vec![("q", query.to_string())];
177
178 if let Some(limit) = options.limit {
179 params.push(("limit", limit.to_string()));
180 }
181 if let Some(lang) = &options.language {
182 params.push(("language", lang.clone()));
183 }
184 if !options.country_codes.is_empty() {
185 let codes: String = options.country_codes.join(",");
186 params.push(("country", codes));
187 }
188 if let Some(bbox) = &options.bounding_box {
189 params.push((
190 "bbox",
191 format!(
192 "{},{},{},{}",
193 bbox.south_west.lng,
194 bbox.south_west.lat,
195 bbox.north_east.lng,
196 bbox.north_east.lat
197 ),
198 ));
199 }
200 if let Some(extra) = &options.provider_extra {
201 if let Some(obj) = extra.as_object() {
202 if let Some(v) = obj.get("proximity").and_then(|v| v.as_str()) {
203 params.push(("proximity", v.to_string()));
204 }
205 if let Some(v) = obj.get("types").and_then(|v| v.as_str()) {
206 params.push(("types", v.to_string()));
207 }
208 if let Some(v) = obj.get("worldview").and_then(|v| v.as_str()) {
209 params.push(("worldview", v.to_string()));
210 }
211 }
212 }
213
214 let builder = self
215 .client
216 .build_request(reqwest::Method::GET, &url)
217 .query(¶ms);
218
219 let result: MapBoxSearchResponse = self.client.request_json(builder).await?;
220 Ok(result.into())
221 }
222
223 async fn reverse_geocode(
224 &self,
225 coordinate: &Coordinate,
226 options: &ReverseGeocodeOptions,
227 ) -> EveryMapResult<SearchResponse> {
228 let url = format!("{}/search/geocode/v6/reverse", self.base_url);
229 let mut params: Vec<(&str, String)> = vec![
230 ("longitude", coordinate.lng.to_string()),
231 ("latitude", coordinate.lat.to_string()),
232 ];
233
234 if let Some(limit) = options.limit {
235 params.push(("limit", limit.to_string()));
236 }
237 if let Some(lang) = &options.language {
238 params.push(("language", lang.clone()));
239 }
240 if let Some(radius) = options.radius {
241 log::warn!(
242 "MapBox Reverse Geocoding v6 does not support radius constraints; \
243 radius ({}) will be ignored",
244 radius
245 );
246 }
247 if let Some(extra) = &options.provider_extra {
248 if let Some(obj) = extra.as_object() {
249 if let Some(v) = obj.get("types").and_then(|v| v.as_str()) {
250 params.push(("types", v.to_string()));
251 }
252 if let Some(v) = obj.get("worldview").and_then(|v| v.as_str()) {
253 params.push(("worldview", v.to_string()));
254 }
255 }
256 }
257
258 let builder = self
259 .client
260 .build_request(reqwest::Method::GET, &url)
261 .query(¶ms);
262
263 let result: MapBoxSearchResponse = self.client.request_json(builder).await?;
264 Ok(result.into())
265 }
266}