geocoding_async/openstreetmap.rs
1//! The [OpenStreetMap Nominatim](https://nominatim.org/) provider.
2//!
3//! Geocoding methods are implemented on the [`Openstreetmap`](struct.Openstreetmap.html) struct.
4//! Please see the [API documentation](https://nominatim.org/release-docs/develop/) for details.
5//!
6//! While OpenStreetMap's Nominatim API is free, see the [Nominatim Usage Policy](https://operations.osmfoundation.org/policies/nominatim/)
7//! for details on usage requirements, including a maximum of 1 request per second.
8//!
9//! ### Example
10//!
11//! ```
12//! # tokio_test::block_on(async {
13//! use geocoding_async::{Openstreetmap, Forward, Point};
14//!
15//! let osm = Openstreetmap::new();
16//! let address = "Schwabing, München";
17//! let res = osm.forward(&address).await;
18//! assert_eq!(res.unwrap(), vec![Point::new(11.5884858, 48.1700887)]);
19//! # });
20//! ```
21use crate::GeocodingError;
22use crate::InputBounds;
23use crate::Point;
24use crate::UA_STRING;
25use crate::{Client, HeaderMap, HeaderValue, USER_AGENT};
26use crate::{Deserialize, Serialize};
27use crate::{Forward, Reverse};
28use num_traits::Float;
29use std::fmt::Debug;
30
31/// An instance of the Openstreetmap geocoding service
32pub struct Openstreetmap {
33 client: Client,
34 endpoint: String,
35}
36
37/// An instance of a parameter builder for Openstreetmap geocoding
38pub struct OpenstreetmapParams<'a, T>
39where
40 T: Float + Debug,
41{
42 query: &'a str,
43 addressdetails: bool,
44 viewbox: Option<&'a InputBounds<T>>,
45}
46
47impl<'a, T> OpenstreetmapParams<'a, T>
48where
49 T: Float + Debug,
50{
51 /// Create a new OpenStreetMap parameter builder
52 /// # Example:
53 ///
54 /// ```
55 /// use geocoding_async::{Openstreetmap, InputBounds, Point};
56 /// use geocoding_async::openstreetmap::{OpenstreetmapParams};
57 ///
58 /// let viewbox = InputBounds::new(
59 /// (-0.13806939125061035, 51.51989264641164),
60 /// (-0.13427138328552246, 51.52319711775629),
61 /// );
62 /// let params = OpenstreetmapParams::new(&"UCL Centre for Advanced Spatial Analysis")
63 /// .with_addressdetails(true)
64 /// .with_viewbox(&viewbox)
65 /// .build();
66 /// ```
67 pub fn new(query: &'a str) -> OpenstreetmapParams<'a, T> {
68 OpenstreetmapParams {
69 query,
70 addressdetails: false,
71 viewbox: None,
72 }
73 }
74
75 /// Set the `addressdetails` property
76 pub fn with_addressdetails(&mut self, addressdetails: bool) -> &mut Self {
77 self.addressdetails = addressdetails;
78 self
79 }
80
81 /// Set the `viewbox` property
82 pub fn with_viewbox(&mut self, viewbox: &'a InputBounds<T>) -> &mut Self {
83 self.viewbox = Some(viewbox);
84 self
85 }
86
87 /// Build and return an instance of OpenstreetmapParams
88 pub fn build(&self) -> OpenstreetmapParams<'a, T> {
89 OpenstreetmapParams {
90 query: self.query,
91 addressdetails: self.addressdetails,
92 viewbox: self.viewbox,
93 }
94 }
95}
96
97impl Openstreetmap {
98 /// Create a new Openstreetmap geocoding instance using the default endpoint
99 pub fn new() -> Self {
100 Openstreetmap::new_with_endpoint("https://nominatim.openstreetmap.org/".to_string())
101 }
102
103 /// Create a new Openstreetmap geocoding instance with a custom endpoint.
104 ///
105 /// Endpoint should include a trailing slash (i.e. "https://nominatim.openstreetmap.org/")
106 pub fn new_with_endpoint(endpoint: String) -> Self {
107 let mut headers = HeaderMap::new();
108 headers.insert(USER_AGENT, HeaderValue::from_static(UA_STRING));
109 let client = Client::builder()
110 .default_headers(headers)
111 .build()
112 .expect("Couldn't build a client!");
113 Openstreetmap { client, endpoint }
114 }
115
116 /// A forward-geocoding lookup of an address, returning a full detailed response
117 ///
118 /// Accepts an [`OpenstreetmapParams`](struct.OpenstreetmapParams.html) struct for specifying
119 /// options, including whether to include address details in the response and whether to filter
120 /// by a bounding box.
121 ///
122 /// Please see [the documentation](https://nominatim.org/release-docs/develop/api/Search/) for details.
123 ///
124 /// This method passes the `format` parameter to the API.
125 ///
126 /// # Examples
127 ///
128 /// ```
129 /// # tokio_test::block_on(async {
130 /// use geocoding_async::{Openstreetmap, InputBounds, Point};
131 /// use geocoding_async::openstreetmap::{OpenstreetmapParams, OpenstreetmapResponse};
132 ///
133 /// let osm = Openstreetmap::new();
134 /// let viewbox = InputBounds::new(
135 /// (-0.13806939125061035, 51.51989264641164),
136 /// (-0.13427138328552246, 51.52319711775629),
137 /// );
138 /// let params = OpenstreetmapParams::new(&"UCL Centre for Advanced Spatial Analysis")
139 /// .with_addressdetails(true)
140 /// .with_viewbox(&viewbox)
141 /// .build();
142 /// let res: OpenstreetmapResponse<f64> = osm.forward_full(¶ms).await.unwrap();
143 /// let result = res.features[0].properties.clone();
144 /// assert!(result.display_name.contains("Tottenham Court Road"));
145 /// # });
146 /// ```
147 pub async fn forward_full<T>(
148 &self,
149 params: &OpenstreetmapParams<'_, T>,
150 ) -> Result<OpenstreetmapResponse<T>, GeocodingError>
151 where
152 T: Float + Debug,
153 for<'de> T: Deserialize<'de>,
154 {
155 let format = String::from("geojson");
156 let addressdetails = String::from(if params.addressdetails { "1" } else { "0" });
157 // For lifetime issues
158 let viewbox;
159
160 let mut query = vec![
161 (&"q", params.query),
162 (&"format", &format),
163 (&"addressdetails", &addressdetails),
164 ];
165
166 if let Some(vb) = params.viewbox {
167 viewbox = String::from(*vb);
168 query.push((&"viewbox", &viewbox));
169 }
170
171 let resp = self
172 .client
173 .get(&format!("{}search", self.endpoint))
174 .query(&query)
175 .send()
176 .await?
177 .error_for_status()?;
178 let res: OpenstreetmapResponse<T> = resp.json().await?;
179 Ok(res)
180 }
181}
182
183impl Default for Openstreetmap {
184 fn default() -> Self {
185 Self::new()
186 }
187}
188
189impl<T> Forward<T> for Openstreetmap
190where
191 T: Float + Debug,
192 for<'de> T: Deserialize<'de>,
193{
194 /// A forward-geocoding lookup of an address. Please see [the documentation](https://nominatim.org/release-docs/develop/api/Search/) for details.
195 ///
196 /// This method passes the `format` parameter to the API.
197 async fn forward(&self, place: &str) -> Result<Vec<Point<T>>, GeocodingError> {
198 let resp = self
199 .client
200 .get(&format!("{}search", self.endpoint))
201 .query(&[(&"q", place), (&"format", &String::from("geojson"))])
202 .send()
203 .await?
204 .error_for_status()?;
205 let res: OpenstreetmapResponse<T> = resp.json().await?;
206 Ok(res
207 .features
208 .iter()
209 .map(|res| Point::new(res.geometry.coordinates.0, res.geometry.coordinates.1))
210 .collect())
211 }
212}
213
214impl<T> Reverse<T> for Openstreetmap
215where
216 T: Float + Debug,
217 for<'de> T: Deserialize<'de>,
218{
219 /// A reverse lookup of a point. More detail on the format of the
220 /// returned `String` can be found [here](https://nominatim.org/release-docs/develop/api/Reverse/)
221 ///
222 /// This method passes the `format` parameter to the API.
223 async fn reverse(&self, point: &Point<T>) -> Result<Option<String>, GeocodingError> {
224 let resp = self
225 .client
226 .get(&format!("{}reverse", self.endpoint))
227 .query(&[
228 (&"lon", &point.x().to_f64().unwrap().to_string()),
229 (&"lat", &point.y().to_f64().unwrap().to_string()),
230 (&"format", &String::from("geojson")),
231 ])
232 .send()
233 .await?
234 .error_for_status()?;
235 let res: OpenstreetmapResponse<T> = resp.json().await?;
236 let address = &res.features[0];
237 Ok(Some(address.properties.display_name.to_string()))
238 }
239}
240
241/// The top-level full GeoJSON response returned by a forward-geocoding request
242///
243/// See [the documentation](https://nominatim.org/release-docs/develop/api/Search/#geojson) for more details
244///
245///```json
246///{
247/// "type": "FeatureCollection",
248/// "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
249/// "features": [
250/// {
251/// "type": "Feature",
252/// "properties": {
253/// "place_id": 263681481,
254/// "osm_type": "way",
255/// "osm_id": 355421084,
256/// "display_name": "68, Carrer de Calatrava, les Tres Torres, Sarrià - Sant Gervasi, Barcelona, BCN, Catalonia, 08017, Spain",
257/// "place_rank": 30,
258/// "category": "building",
259/// "type": "apartments",
260/// "importance": 0.7409999999999999,
261/// "address": {
262/// "house_number": "68",
263/// "road": "Carrer de Calatrava",
264/// "suburb": "les Tres Torres",
265/// "city_district": "Sarrià - Sant Gervasi",
266/// "city": "Barcelona",
267/// "county": "BCN",
268/// "state": "Catalonia",
269/// "postcode": "08017",
270/// "country": "Spain",
271/// "country_code": "es"
272/// }
273/// },
274/// "bbox": [
275/// 2.1284918,
276/// 41.401227,
277/// 2.128952,
278/// 41.4015815
279/// ],
280/// "geometry": {
281/// "type": "Point",
282/// "coordinates": [
283/// 2.12872241167437,
284/// 41.40140675
285/// ]
286/// }
287/// }
288/// ]
289///}
290///```
291#[derive(Debug, Serialize, Deserialize)]
292pub struct OpenstreetmapResponse<T>
293where
294 T: Float + Debug,
295{
296 pub r#type: String,
297 pub licence: String,
298 pub features: Vec<OpenstreetmapResult<T>>,
299}
300
301/// A geocoding result
302#[derive(Debug, Serialize, Deserialize)]
303pub struct OpenstreetmapResult<T>
304where
305 T: Float + Debug,
306{
307 pub r#type: String,
308 pub properties: ResultProperties,
309 pub bbox: (T, T, T, T),
310 pub geometry: ResultGeometry<T>,
311}
312
313/// Geocoding result properties
314#[derive(Clone, Debug, Serialize, Deserialize)]
315pub struct ResultProperties {
316 pub place_id: u64,
317 pub osm_type: String,
318 pub osm_id: u64,
319 pub display_name: String,
320 pub place_rank: u64,
321 pub category: String,
322 pub r#type: String,
323 pub importance: f64,
324 pub address: Option<AddressDetails>,
325}
326
327/// Address details in the result object
328#[derive(Clone, Debug, Serialize, Deserialize)]
329pub struct AddressDetails {
330 pub city: Option<String>,
331 pub city_district: Option<String>,
332 pub construction: Option<String>,
333 pub continent: Option<String>,
334 pub country: Option<String>,
335 pub country_code: Option<String>,
336 pub house_number: Option<String>,
337 pub neighbourhood: Option<String>,
338 pub postcode: Option<String>,
339 pub public_building: Option<String>,
340 pub state: Option<String>,
341 pub suburb: Option<String>,
342 pub road: Option<String>,
343 pub village: Option<String>,
344}
345
346/// A geocoding result geometry
347#[derive(Debug, Serialize, Deserialize)]
348pub struct ResultGeometry<T>
349where
350 T: Float + Debug,
351{
352 pub r#type: String,
353 pub coordinates: (T, T),
354}
355
356#[cfg(test)]
357mod test {
358 use super::*;
359
360 #[tokio::test]
361 async fn new_with_endpoint_forward_test() {
362 let osm =
363 Openstreetmap::new_with_endpoint("https://nominatim.openstreetmap.org/".to_string());
364 let address = "Schwabing, München";
365 let res = osm.forward(&address).await;
366 assert_eq!(res.unwrap(), vec![Point::new(11.5884858, 48.1700887)]);
367 }
368
369 #[tokio::test]
370 async fn forward_full_test() {
371 let osm = Openstreetmap::new();
372 let viewbox = InputBounds::new(
373 (-0.13806939125061035, 51.51989264641164),
374 (-0.13427138328552246, 51.52319711775629),
375 );
376 let params = OpenstreetmapParams::new(&"UCL Centre for Advanced Spatial Analysis")
377 .with_addressdetails(true)
378 .with_viewbox(&viewbox)
379 .build();
380 let res: OpenstreetmapResponse<f64> = osm.forward_full(¶ms).await.unwrap();
381 let result = res.features[0].properties.clone();
382 assert!(result.display_name.contains("Tottenham Court Road"));
383 assert_eq!(result.address.unwrap().city.unwrap(), "London");
384 }
385
386 #[tokio::test]
387 async fn forward_test() {
388 let osm = Openstreetmap::new();
389 let address = "Schwabing, München";
390 let res = osm.forward(&address).await;
391 assert_eq!(res.unwrap(), vec![Point::new(11.5884858, 48.1700887)]);
392 }
393
394 #[tokio::test]
395 async fn reverse_test() {
396 let osm = Openstreetmap::new();
397 let p = Point::new(2.12870, 41.40139);
398 let res = osm.reverse(&p).await;
399 assert!(res
400 .unwrap()
401 .unwrap()
402 .contains("Barcelona, Barcelonès, Barcelona, Catalunya"));
403 }
404}