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": "&#x20AC;",
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}