geocoding_async/opencage.rs
1//! The [OpenCage Geocoding](https://opencagedata.com/) provider.
2//!
3//! Geocoding methods are implemented on the [`Opencage`](struct.Opencage.html) struct.
4//! Please see the [API documentation](https://opencagedata.com/api) for details.
5//! Note that rate limits apply to the free tier:
6//! there is a [rate-limit](https://opencagedata.com/api#rate-limiting) of 1 request per second,
7//! and a quota of calls allowed per 24-hour period. The remaining daily quota can be retrieved
8//! using the [`remaining_calls()`](struct.Opencage.html#method.remaining_calls) method. If you
9//! are a paid tier user, this value will not be updated, and will remain `None`.
10//! ### A Note on Coordinate Order
11//! This provider's API documentation shows all coordinates in `[Latitude, Longitude]` order.
12//! However, `Geocoding` requires input `Point` coordinate order as `[Longitude, Latitude]`
13//! `(x, y)`, and returns coordinates with that order.
14//!
15//! ### Example
16//!
17//! ```
18//! # tokio_test::block_on(async {
19//! use geocoding_async::{Opencage, Point, Reverse};
20//!
21//! let mut oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
22//! oc.parameters.language = Some("fr");
23//! let p = Point::new(2.12870, 41.40139);
24//! let res = oc.reverse(&p).await;
25//! // "Carrer de Calatrava, 68, 08017 Barcelone, Espagne"
26//! println!("{:?}", res.unwrap());
27//! # });
28//! ```
29use crate::DeserializeOwned;
30use crate::GeocodingError;
31use crate::InputBounds;
32use crate::Point;
33use crate::UA_STRING;
34use crate::{Client, HeaderMap, HeaderValue, USER_AGENT};
35use crate::{Deserialize, Serialize};
36use crate::{Forward, Reverse};
37use num_traits::Float;
38use serde::Deserializer;
39use std::collections::HashMap;
40use std::fmt::Debug;
41use std::sync::{Arc, Mutex};
42
43macro_rules! add_optional_param {
44 ($query:expr, $param:expr, $name:expr) => {
45 if let Some(p) = $param {
46 $query.push(($name, p))
47 }
48 };
49}
50
51// Please see the [API documentation](https://opencagedata.com/api#forward-opt) for details.
52#[derive(Default)]
53pub struct Parameters<'a> {
54 pub language: Option<&'a str>,
55 pub countrycode: Option<&'a str>,
56 pub limit: Option<&'a str>,
57}
58
59impl<'a> Parameters<'a> {
60 fn as_query(&self) -> Vec<(&'a str, &'a str)> {
61 let mut query = vec![];
62 add_optional_param!(query, self.language, "language");
63 add_optional_param!(query, self.countrycode, "countrycode");
64 add_optional_param!(query, self.limit, "limit");
65 query
66 }
67}
68
69pub fn deserialize_string_or_int<'de, D>(deserializer: D) -> Result<String, D::Error>
70where
71 D: Deserializer<'de>,
72{
73 #[derive(Deserialize)]
74 #[serde(untagged)]
75 enum StringOrInt {
76 String(String),
77 Int(i32),
78 }
79
80 match StringOrInt::deserialize(deserializer)? {
81 StringOrInt::String(s) => Ok(s),
82 StringOrInt::Int(i) => Ok(i.to_string()),
83 }
84}
85
86// OpenCage has a custom rate-limit header, indicating remaining calls
87// header! { (XRatelimitRemaining, "X-RateLimit-Remaining") => [i32] }
88static XRL: &str = "x-ratelimit-remaining";
89/// Use this constant if you don't need to restrict a `forward_full` call with a bounding box
90pub static NOBOX: Option<InputBounds<f64>> = None::<InputBounds<f64>>;
91
92/// An instance of the Opencage Geocoding service
93pub struct Opencage<'a> {
94 api_key: String,
95 client: Client,
96 endpoint: String,
97 pub parameters: Parameters<'a>,
98 remaining: Arc<Mutex<Option<i32>>>,
99}
100
101impl<'a> Opencage<'a> {
102 /// Create a new OpenCage geocoding instance
103 pub fn new(api_key: String) -> Self {
104 let mut headers = HeaderMap::new();
105 headers.insert(USER_AGENT, HeaderValue::from_static(UA_STRING));
106 let client = Client::builder()
107 .default_headers(headers)
108 .build()
109 .expect("Couldn't build a client!");
110
111 let parameters = Parameters::default();
112 Opencage {
113 api_key,
114 client,
115 parameters,
116 endpoint: "https://api.opencagedata.com/geocode/v1/json".to_string(),
117 remaining: Arc::new(Mutex::new(None)),
118 }
119 }
120 /// Retrieve the remaining API calls in your daily quota
121 ///
122 /// Initially, this value is `None`. Any OpenCage API call using a "Free Tier" key
123 /// will update this value to reflect the remaining quota for the API key.
124 /// See the [API docs](https://opencagedata.com/api#rate-limiting) for details.
125 pub fn remaining_calls(&self) -> Option<i32> {
126 *self.remaining.lock().unwrap()
127 }
128 /// A reverse lookup of a point, returning an annotated response.
129 ///
130 /// This method passes the `no_record` parameter to the API.
131 ///
132 /// # Examples
133 ///
134 ///```
135 /// # tokio_test::block_on(async {
136 /// use geocoding_async::{Opencage, Point};
137 ///
138 /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
139 /// let p = Point::new(2.12870, 41.40139);
140 /// // a full `OpencageResponse` struct
141 /// let res = oc.reverse_full(&p).await.unwrap();
142 /// // responses may include multiple results
143 /// let first_result = &res.results[0];
144 /// assert_eq!(
145 /// first_result.components["road"],
146 /// "Carrer de Calatrava"
147 /// );
148 /// # });
149 ///```
150 pub async fn reverse_full<T>(
151 &self,
152 point: &Point<T>,
153 ) -> Result<OpencageResponse<T>, GeocodingError>
154 where
155 T: Float + DeserializeOwned + Debug,
156 {
157 let q = format!(
158 "{}, {}",
159 // OpenCage expects lat, lon order
160 (&point.y().to_f64().unwrap().to_string()),
161 &point.x().to_f64().unwrap().to_string()
162 );
163 let mut query = vec![
164 ("q", q.as_str()),
165 ("key", &self.api_key),
166 ("no_annotations", "0"),
167 ("no_record", "1"),
168 ];
169 query.extend(self.parameters.as_query());
170
171 let resp = self
172 .client
173 .get(&self.endpoint)
174 .query(&query)
175 .send()
176 .await?
177 .error_for_status()?;
178 // it's OK to index into this vec, because reverse-geocoding only returns a single result
179 if let Some(headers) = resp.headers().get::<_>(XRL) {
180 let mut lock = self.remaining.try_lock();
181 if let Ok(ref mut mutex) = lock {
182 // not ideal, but typed headers are currently impossible in 0.9.x
183 let h = headers.to_str()?;
184 let h: i32 = h.parse()?;
185 **mutex = Some(h)
186 }
187 }
188 let res: OpencageResponse<T> = resp.json().await?;
189 Ok(res)
190 }
191 /// A forward-geocoding lookup of an address, returning an annotated response.
192 ///
193 /// it is recommended that you restrict the search space by passing a
194 /// [bounding box](struct.InputBounds.html) to search within.
195 /// If you don't need or want to restrict the search using a bounding box (usually not recommended), you
196 /// may pass the [`NOBOX`](static.NOBOX.html) static value instead.
197 ///
198 /// Please see [the documentation](https://opencagedata.com/api#ambiguous-results) for details
199 /// of best practices in order to obtain good-quality results.
200 ///
201 /// This method passes the `no_record` parameter to the API.
202 ///
203 /// # Examples
204 ///
205 ///```
206 /// # tokio_test::block_on(async {
207 /// use geocoding_async::{Opencage, InputBounds, Point};
208 ///
209 /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
210 /// let address = "UCL Centre for Advanced Spatial Analysis";
211 /// // Optionally restrict the search space using a bounding box.
212 /// // The first point is the bottom-left corner, the second is the top-right.
213 /// let bbox = InputBounds::new(
214 /// Point::new(-0.13806939125061035, 51.51989264641164),
215 /// Point::new(-0.13427138328552246, 51.52319711775629),
216 /// );
217 /// let res = oc.forward_full(&address, bbox).await.unwrap();
218 /// let first_result = &res.results[0];
219 /// // the first result is correct
220 /// assert!(first_result.formatted.contains("90 Tottenham Court Road"));
221 /// # });
222 ///```
223 ///
224 /// ```
225 /// # tokio_test::block_on(async {
226 /// // You can pass NOBOX if you don't need bounds.
227 /// use geocoding_async::{Opencage, InputBounds, Point};
228 /// use geocoding_async::opencage::{NOBOX};
229 /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
230 /// let address = "Moabit, Berlin";
231 /// let res = oc.forward_full(&address, NOBOX).await.unwrap();
232 /// let first_result = &res.results[0];
233 /// assert_eq!(
234 /// first_result.formatted,
235 /// "Moabit, Berlin, Germany"
236 /// );
237 /// # });
238 /// ```
239 ///
240 /// ```
241 /// # tokio_test::block_on(async {
242 /// // There are several ways to construct a Point, such as from a tuple
243 /// use geocoding_async::{Opencage, InputBounds, Point};
244 /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
245 /// let address = "UCL Centre for Advanced Spatial Analysis";
246 /// let bbox = InputBounds::new(
247 /// (-0.13806939125061035, 51.51989264641164),
248 /// (-0.13427138328552246, 51.52319711775629),
249 /// );
250 /// let res = oc.forward_full(&address, bbox).await.unwrap();
251 /// let first_result = &res.results[0];
252 /// assert!(
253 /// first_result.formatted.contains(
254 /// "90 Tottenham Court Road"
255 /// ));
256 /// # });
257 /// ```
258 pub async fn forward_full<T, U>(
259 &self,
260 place: &str,
261 bounds: U,
262 ) -> Result<OpencageResponse<T>, GeocodingError>
263 where
264 T: Float + DeserializeOwned + Debug,
265 U: Into<Option<InputBounds<T>>>,
266 {
267 let ann = String::from("0");
268 let record = String::from("1");
269 // we need this to avoid lifetime inconvenience
270 let bd;
271 let mut query = vec![
272 ("q", place),
273 ("key", &self.api_key),
274 ("no_annotations", &ann),
275 ("no_record", &record),
276 ];
277
278 // If search bounds are passed, use them
279 if let Some(bds) = bounds.into() {
280 bd = String::from(bds);
281 query.push(("bounds", &bd));
282 }
283 query.extend(self.parameters.as_query());
284
285 let resp = self
286 .client
287 .get(&self.endpoint)
288 .query(&query)
289 .send()
290 .await?
291 .error_for_status()?;
292 if let Some(headers) = resp.headers().get::<_>(XRL) {
293 let mut lock = self.remaining.try_lock();
294 if let Ok(ref mut mutex) = lock {
295 // not ideal, but typed headers are currently impossible in 0.9.x
296 let h = headers.to_str()?;
297 let h: i32 = h.parse()?;
298 **mutex = Some(h)
299 }
300 }
301 let res: OpencageResponse<T> = resp.json().await?;
302 Ok(res)
303 }
304}
305
306impl<'a, T> Reverse<T> for Opencage<'a>
307where
308 T: Float + DeserializeOwned + Debug,
309{
310 /// A reverse lookup of a point. More detail on the format of the
311 /// returned `String` can be found [here](https://blog.opencagedata.com/post/99059889253/good-looking-addresses-solving-the-berlin-berlin)
312 ///
313 /// This method passes the `no_annotations` and `no_record` parameters to the API.
314 async fn reverse(&self, point: &Point<T>) -> Result<Option<String>, GeocodingError> {
315 let q = format!(
316 "{}, {}",
317 // OpenCage expects lat, lon order
318 (&point.y().to_f64().unwrap().to_string()),
319 &point.x().to_f64().unwrap().to_string()
320 );
321 let mut query = vec![
322 ("q", q.as_str()),
323 ("key", &self.api_key),
324 ("no_annotations", "1"),
325 ("no_record", "1"),
326 ];
327 query.extend(self.parameters.as_query());
328
329 let resp = self
330 .client
331 .get(&self.endpoint)
332 .query(&query)
333 .send()
334 .await?
335 .error_for_status()?;
336 if let Some(headers) = resp.headers().get::<_>(XRL) {
337 let mut lock = self.remaining.try_lock();
338 if let Ok(ref mut mutex) = lock {
339 // not ideal, but typed headers are currently impossible in 0.9.x
340 let h = headers.to_str()?;
341 let h: i32 = h.parse()?;
342 **mutex = Some(h)
343 }
344 }
345 let res: OpencageResponse<T> = resp.json().await?;
346 // it's OK to index into this vec, because reverse-geocoding only returns a single result
347 let address = &res.results[0];
348 Ok(Some(address.formatted.to_string()))
349 }
350}
351
352impl<'a, T> Forward<T> for Opencage<'a>
353where
354 T: Float + DeserializeOwned + Debug,
355{
356 /// A forward-geocoding lookup of an address. Please see [the documentation](https://opencagedata.com/api#ambiguous-results) for details
357 /// of best practices in order to obtain good-quality results.
358 ///
359 /// This method passes the `no_annotations` and `no_record` parameters to the API.
360 async fn forward(&self, place: &str) -> Result<Vec<Point<T>>, GeocodingError> {
361 let mut query = vec![
362 ("q", place),
363 ("key", &self.api_key),
364 ("no_annotations", "1"),
365 ("no_record", "1"),
366 ];
367 query.extend(self.parameters.as_query());
368
369 let resp = self
370 .client
371 .get(&self.endpoint)
372 .query(&query)
373 .send()
374 .await?
375 .error_for_status()?;
376 if let Some(headers) = resp.headers().get::<_>(XRL) {
377 let mut lock = self.remaining.try_lock();
378 if let Ok(ref mut mutex) = lock {
379 // not ideal, but typed headers are currently impossible in 0.9.x
380 let h = headers.to_str()?;
381 let h: i32 = h.parse()?;
382 **mutex = Some(h)
383 }
384 }
385 let res: OpencageResponse<T> = resp.json().await?;
386 Ok(res
387 .results
388 .iter()
389 .map(|res| Point::new(res.geometry["lng"], res.geometry["lat"]))
390 .collect())
391 }
392}
393
394/// The top-level full JSON response returned by a forward-geocoding request
395///
396/// See [the documentation](https://opencagedata.com/api#response) for more details
397///
398///```json
399/// {
400/// "documentation": "https://opencagedata.com/api",
401/// "licenses": [
402/// {
403/// "name": "CC-BY-SA",
404/// "url": "http://creativecommons.org/licenses/by-sa/3.0/"
405/// },
406/// {
407/// "name": "ODbL",
408/// "url": "http://opendatacommons.org/licenses/odbl/summary/"
409/// }
410/// ],
411/// "rate": {
412/// "limit": 2500,
413/// "remaining": 2499,
414/// "reset": 1523318400
415/// },
416/// "results": [
417/// {
418/// "annotations": {
419/// "DMS": {
420/// "lat": "41° 24' 5.06412'' N",
421/// "lng": "2° 7' 43.40064'' E"
422/// },
423/// "MGRS": "31TDF2717083684",
424/// "Maidenhead": "JN11bj56ki",
425/// "Mercator": {
426/// "x": 236968.295,
427/// "y": 5043465.71
428/// },
429/// "OSM": {
430/// "edit_url": "https://www.openstreetmap.org/edit?way=355421084#map=17/41.40141/2.12872",
431/// "url": "https://www.openstreetmap.org/?mlat=41.40141&mlon=2.12872#map=17/41.40141/2.12872"
432/// },
433/// "callingcode": 34,
434/// "currency": {
435/// "alternate_symbols": [
436///
437/// ],
438/// "decimal_mark": ",",
439/// "html_entity": "€",
440/// "iso_code": "EUR",
441/// "iso_numeric": 978,
442/// "name": "Euro",
443/// "smallest_denomination": 1,
444/// "subunit": "Cent",
445/// "subunit_to_unit": 100,
446/// "symbol": "€",
447/// "symbol_first": 1,
448/// "thousands_separator": "."
449/// },
450/// "flag": "🇪🇸",
451/// "geohash": "sp3e82yhdvd7p5x1mbdv",
452/// "qibla": 110.53,
453/// "sun": {
454/// "rise": {
455/// "apparent": 1523251260,
456/// "astronomical": 1523245440,
457/// "civil": 1523249580,
458/// "nautical": 1523247540
459/// },
460/// "set": {
461/// "apparent": 1523298360,
462/// "astronomical": 1523304180,
463/// "civil": 1523300040,
464/// "nautical": 1523302080
465/// }
466/// },
467/// "timezone": {
468/// "name": "Europe/Madrid",
469/// "now_in_dst": 1,
470/// "offset_sec": 7200,
471/// "offset_string": 200,
472/// "short_name": "CEST"
473/// },
474/// "what3words": {
475/// "words": "chins.pictures.passes"
476/// }
477/// },
478/// "bounds": {
479/// "northeast": {
480/// "lat": 41.4015815,
481/// "lng": 2.128952
482/// },
483/// "southwest": {
484/// "lat": 41.401227,
485/// "lng": 2.1284918
486/// }
487/// },
488/// "components": {
489/// "ISO_3166-1_alpha-2": "ES",
490/// "_type": "building",
491/// "city": "Barcelona",
492/// "city_district": "Sarrià - Sant Gervasi",
493/// "country": "Spain",
494/// "country_code": "es",
495/// "county": "BCN",
496/// "house_number": "68",
497/// "political_union": "European Union",
498/// "postcode": "08017",
499/// "road": "Carrer de Calatrava",
500/// "state": "Catalonia",
501/// "suburb": "les Tres Torres"
502/// },
503/// "confidence": 10,
504/// "formatted": "Carrer de Calatrava, 68, 08017 Barcelona, Spain",
505/// "geometry": {
506/// "lat": 41.4014067,
507/// "lng": 2.1287224
508/// }
509/// }
510/// ],
511/// "status": {
512/// "code": 200,
513/// "message": "OK"
514/// },
515/// "stay_informed": {
516/// "blog": "https://blog.opencagedata.com",
517/// "twitter": "https://twitter.com/opencagedata"
518/// },
519/// "thanks": "For using an OpenCage Data API",
520/// "timestamp": {
521/// "created_http": "Mon, 09 Apr 2018 12:33:01 GMT",
522/// "created_unix": 1523277181
523/// },
524/// "total_results": 1
525/// }
526///```
527#[derive(Debug, Serialize, Deserialize)]
528pub struct OpencageResponse<T>
529where
530 T: Float,
531{
532 pub documentation: String,
533 pub licenses: Vec<HashMap<String, String>>,
534 pub rate: Option<HashMap<String, i32>>,
535 pub results: Vec<Results<T>>,
536 pub status: Status,
537 pub stay_informed: HashMap<String, String>,
538 pub thanks: String,
539 pub timestamp: Timestamp,
540 pub total_results: i32,
541}
542
543/// A forward geocoding result
544#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct Results<T>
546where
547 T: Float,
548{
549 pub annotations: Option<Annotations<T>>,
550 pub bounds: Option<Bounds<T>>,
551 pub components: HashMap<String, serde_json::Value>,
552 pub confidence: i8,
553 pub formatted: String,
554 pub geometry: HashMap<String, T>,
555}
556
557/// Annotations pertaining to the geocoding result
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct Annotations<T>
560where
561 T: Float,
562{
563 pub dms: Option<HashMap<String, String>>,
564 pub mgrs: Option<String>,
565 pub maidenhead: Option<String>,
566 pub mercator: Option<HashMap<String, T>>,
567 pub osm: Option<HashMap<String, String>>,
568 pub callingcode: i16,
569 pub currency: Option<Currency>,
570 pub flag: String,
571 pub geohash: String,
572 pub qibla: T,
573 pub sun: Sun,
574 pub timezone: Timezone,
575 pub what3words: HashMap<String, String>,
576}
577
578/// Currency metadata
579#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct Currency {
581 pub alternate_symbols: Option<Vec<String>>,
582 pub decimal_mark: String,
583 pub html_entity: String,
584 pub iso_code: String,
585 #[serde(deserialize_with = "deserialize_string_or_int")]
586 pub iso_numeric: String,
587 pub name: String,
588 pub smallest_denomination: i16,
589 pub subunit: String,
590 pub subunit_to_unit: i16,
591 pub symbol: String,
592 pub symbol_first: i16,
593 pub thousands_separator: String,
594}
595
596/// Sunrise and sunset metadata
597#[derive(Debug, Clone, Serialize, Deserialize)]
598pub struct Sun {
599 pub rise: HashMap<String, i64>,
600 pub set: HashMap<String, i64>,
601}
602
603/// Timezone metadata
604#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct Timezone {
606 pub name: String,
607 pub now_in_dst: i16,
608 pub offset_sec: i32,
609 #[serde(deserialize_with = "deserialize_string_or_int")]
610 pub offset_string: String,
611 #[serde(deserialize_with = "deserialize_string_or_int")]
612 pub short_name: String,
613}
614
615/// HTTP status metadata
616#[derive(Debug, Serialize, Deserialize)]
617pub struct Status {
618 pub message: String,
619 pub code: i16,
620}
621
622/// Timestamp metadata
623#[derive(Debug, Serialize, Deserialize)]
624pub struct Timestamp {
625 pub created_http: String,
626 pub created_unix: UnixTime,
627}
628
629/// Primitive unix timestamp
630#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
631pub struct UnixTime(i64);
632
633impl UnixTime {
634 pub const fn as_seconds(self) -> i64 {
635 self.0
636 }
637 pub const fn from_seconds(seconds: i64) -> Self {
638 Self(seconds)
639 }
640}
641
642/// Bounding-box metadata
643#[derive(Debug, Clone, Serialize, Deserialize)]
644pub struct Bounds<T>
645where
646 T: Float,
647{
648 pub northeast: HashMap<String, T>,
649 pub southwest: HashMap<String, T>,
650}
651
652#[cfg(test)]
653mod test {
654 use super::*;
655 use crate::Coord;
656
657 #[tokio::test]
658 async fn reverse_test() {
659 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
660 let p = Point::new(2.12870, 41.40139);
661 let res = oc.reverse(&p).await;
662 assert_eq!(
663 res.unwrap(),
664 Some("Carrer de Calatrava, 68, 08017 Barcelona, Spain".to_string())
665 );
666 }
667
668 #[tokio::test]
669 async fn reverse_test_with_params() {
670 let mut oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
671 oc.parameters.language = Some("fr");
672 let p = Point::new(2.12870, 41.40139);
673 let res = oc.reverse(&p).await;
674 assert_eq!(
675 res.unwrap(),
676 Some("Carrer de Calatrava, 68, 08017 Barcelone, Espagne".to_string())
677 );
678 }
679
680 #[tokio::test]
681 #[allow(deprecated)]
682 async fn forward_test() {
683 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
684 let address = "Schwabing, München";
685 let res = oc.forward(&address).await;
686 assert_eq!(
687 res.unwrap(),
688 vec![Point(Coord {
689 x: 11.5884858,
690 y: 48.1700887
691 })]
692 );
693 }
694 #[tokio::test]
695 async fn reverse_full_test() {
696 let mut oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
697 oc.parameters.language = Some("fr");
698 let p = Point::new(2.12870, 41.40139);
699 let res = oc.reverse_full(&p).await.unwrap();
700 let first_result = &res.results[0];
701 assert_eq!(first_result.components["road"], "Carrer de Calatrava");
702 }
703 #[tokio::test]
704 async fn forward_full_test() {
705 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
706 let address = "UCL Centre for Advanced Spatial Analysis";
707 let bbox = InputBounds {
708 minimum_lonlat: Point::new(-0.13806939125061035, 51.51989264641164),
709 maximum_lonlat: Point::new(-0.13427138328552246, 51.52319711775629),
710 };
711 let res = oc.forward_full(&address, bbox).await.unwrap();
712 let first_result = &res.results[0];
713 assert!(first_result.formatted.contains("UCL"));
714 }
715 #[tokio::test]
716 async fn forward_full_test_floats() {
717 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
718 let address = "UCL Centre for Advanced Spatial Analysis";
719 let bbox = InputBounds::new(
720 Point::new(-0.13806939125061035, 51.51989264641164),
721 Point::new(-0.13427138328552246, 51.52319711775629),
722 );
723 let res = oc.forward_full(&address, bbox).await.unwrap();
724 let first_result = &res.results[0];
725 assert!(
726 first_result.formatted.contains("UCL")
727 && first_result.formatted.contains("90 Tottenham Court Road")
728 );
729 }
730 #[tokio::test]
731 async fn forward_full_test_pointfrom() {
732 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
733 let address = "UCL Centre for Advanced Spatial Analysis";
734 let bbox = InputBounds::new(
735 Point::from((-0.13806939125061035, 51.51989264641164)),
736 Point::from((-0.13427138328552246, 51.52319711775629)),
737 );
738 let res = oc.forward_full(&address, bbox).await.unwrap();
739 let first_result = &res.results[0];
740 assert!(
741 first_result.formatted.contains("UCL")
742 && first_result.formatted.contains("90 Tottenham Court Road")
743 );
744 }
745 #[tokio::test]
746 async fn forward_full_test_pointinto() {
747 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
748 let address = "UCL Centre for Advanced Spatial Analysis";
749 let bbox = InputBounds::new(
750 (-0.13806939125061035, 51.51989264641164),
751 (-0.13427138328552246, 51.52319711775629),
752 );
753 let res = oc.forward_full(&address, bbox).await.unwrap();
754 let first_result = &res.results[0];
755 assert!(first_result
756 .formatted
757 .contains("Tottenham Court Road, London"));
758 }
759 #[tokio::test]
760 async fn forward_full_test_nobox() {
761 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
762 let address = "Moabit, Berlin, Germany";
763 let res = oc.forward_full(&address, NOBOX).await.unwrap();
764 let first_result = &res.results[0];
765 assert_eq!(first_result.formatted, "Moabit, Berlin, Germany");
766 }
767}