Skip to content

Commit ff48044

Browse files
atymicjbelien
authored andcommitted
feat: add geocode-earth provider (#962)
* feat: add geocode-earth provider This adds a new provider for https://geocode.earth/ Resolves #808 * fix: update tests + add cached responses * fix: update docs * chore: add env to main test config
0 parents  commit ff48044

20 files changed

+716
-0
lines changed

.gitattributes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.gitattributes export-ignore
2+
.travis.yml export-ignore
3+
phpunit.xml.dist export-ignore
4+
Tests/ export-ignore

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml

.travis.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
language: php
2+
sudo: false
3+
4+
php: 7.2
5+
6+
7+
install:
8+
- composer update --prefer-stable --prefer-dist
9+
10+
script:
11+
- composer test-ci
12+
13+
after_success:
14+
- wget https://scrutinizer-ci.com/ocular.phar
15+
- php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml
16+

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Change Log
2+
3+
The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release.
4+
5+
## 1.0.0
6+
7+
First release of this provider.

GeocodeEarth.php

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Geocoder package.
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*
10+
* @license MIT License
11+
*/
12+
13+
namespace Geocoder\Provider\GeocodeEarth;
14+
15+
use Geocoder\Collection;
16+
use Geocoder\Exception\InvalidCredentials;
17+
use Geocoder\Exception\QuotaExceeded;
18+
use Geocoder\Exception\UnsupportedOperation;
19+
use Geocoder\Model\Address;
20+
use Geocoder\Model\AddressCollection;
21+
use Geocoder\Query\GeocodeQuery;
22+
use Geocoder\Query\ReverseQuery;
23+
use Geocoder\Http\Provider\AbstractHttpProvider;
24+
use Geocoder\Provider\Provider;
25+
use Http\Client\HttpClient;
26+
27+
final class GeocodeEarth extends AbstractHttpProvider implements Provider
28+
{
29+
/**
30+
* @var string
31+
*/
32+
const GEOCODE_ENDPOINT_URL = 'https://api.geocode.earth/v1/search?text=%s&api_key=%s&size=%d';
33+
34+
/**
35+
* @var string
36+
*/
37+
const REVERSE_ENDPOINT_URL = 'https://api.geocode.earth/v1/reverse?point.lat=%f&point.lon=%f&api_key=%s&size=%d';
38+
39+
/**
40+
* @var string
41+
*/
42+
private $apiKey;
43+
44+
/**
45+
* @param HttpClient $client an HTTP adapter
46+
* @param string $apiKey an API key
47+
*/
48+
public function __construct(HttpClient $client, string $apiKey)
49+
{
50+
if (empty($apiKey)) {
51+
throw new InvalidCredentials('No API key provided.');
52+
}
53+
54+
$this->apiKey = $apiKey;
55+
parent::__construct($client);
56+
}
57+
58+
/**
59+
* {@inheritdoc}
60+
*/
61+
public function geocodeQuery(GeocodeQuery $query): Collection
62+
{
63+
$address = $query->getText();
64+
65+
// This API doesn't handle IPs
66+
if (filter_var($address, FILTER_VALIDATE_IP)) {
67+
throw new UnsupportedOperation('The GeocodeEarth provider does not support IP addresses, only street addresses.');
68+
}
69+
70+
$url = sprintf(self::GEOCODE_ENDPOINT_URL, urlencode($address), $this->apiKey, $query->getLimit());
71+
72+
return $this->executeQuery($url);
73+
}
74+
75+
/**
76+
* {@inheritdoc}
77+
*/
78+
public function reverseQuery(ReverseQuery $query): Collection
79+
{
80+
$coordinates = $query->getCoordinates();
81+
$longitude = $coordinates->getLongitude();
82+
$latitude = $coordinates->getLatitude();
83+
$url = sprintf(self::REVERSE_ENDPOINT_URL, $latitude, $longitude, $this->apiKey, $query->getLimit());
84+
85+
return $this->executeQuery($url);
86+
}
87+
88+
/**
89+
* {@inheritdoc}
90+
*/
91+
public function getName(): string
92+
{
93+
return 'geocode_earth';
94+
}
95+
96+
/**
97+
* @param $url
98+
*
99+
* @return Collection
100+
*/
101+
private function executeQuery(string $url): AddressCollection
102+
{
103+
$content = $this->getUrlContents($url);
104+
$json = json_decode($content, true);
105+
106+
if (isset($json['meta'])) {
107+
switch ($json['meta']['status_code']) {
108+
case 403:
109+
throw new InvalidCredentials('Invalid or missing api key.');
110+
case 429:
111+
throw new QuotaExceeded('Valid request but quota exceeded.');
112+
}
113+
}
114+
115+
if (!isset($json['type']) || 'FeatureCollection' !== $json['type'] || !isset($json['features']) || 0 === count($json['features'])) {
116+
return new AddressCollection([]);
117+
}
118+
119+
$locations = $json['features'];
120+
121+
if (empty($locations)) {
122+
return new AddressCollection([]);
123+
}
124+
125+
$results = [];
126+
foreach ($locations as $location) {
127+
$bounds = [
128+
'south' => null,
129+
'west' => null,
130+
'north' => null,
131+
'east' => null,
132+
];
133+
if (isset($location['bbox'])) {
134+
$bounds = [
135+
'south' => $location['bbox'][3],
136+
'west' => $location['bbox'][2],
137+
'north' => $location['bbox'][1],
138+
'east' => $location['bbox'][0],
139+
];
140+
}
141+
142+
$props = $location['properties'];
143+
144+
$adminLevels = [];
145+
foreach (['region', 'county', 'locality', 'macroregion', 'country'] as $i => $component) {
146+
if (isset($props[$component])) {
147+
$adminLevels[] = ['name' => $props[$component], 'level' => $i + 1];
148+
}
149+
}
150+
151+
$results[] = Address::createFromArray([
152+
'providedBy' => $this->getName(),
153+
'latitude' => $location['geometry']['coordinates'][1],
154+
'longitude' => $location['geometry']['coordinates'][0],
155+
'bounds' => $bounds,
156+
'streetNumber' => isset($props['housenumber']) ? $props['housenumber'] : null,
157+
'streetName' => isset($props['street']) ? $props['street'] : null,
158+
'subLocality' => isset($props['neighbourhood']) ? $props['neighbourhood'] : null,
159+
'locality' => isset($props['locality']) ? $props['locality'] : null,
160+
'postalCode' => isset($props['postalcode']) ? $props['postalcode'] : null,
161+
'adminLevels' => $adminLevels,
162+
'country' => isset($props['country']) ? $props['country'] : null,
163+
'countryCode' => isset($props['country_a']) ? strtoupper($props['country_a']) : null,
164+
]);
165+
}
166+
167+
return new AddressCollection($results);
168+
}
169+
170+
/**
171+
* @param array $components
172+
*
173+
* @return null|string
174+
*/
175+
protected function guessLocality(array $components)
176+
{
177+
$localityKeys = ['city', 'town', 'village', 'hamlet'];
178+
179+
return $this->guessBestComponent($components, $localityKeys);
180+
}
181+
182+
/**
183+
* @param array $components
184+
*
185+
* @return null|string
186+
*/
187+
protected function guessStreetName(array $components)
188+
{
189+
$streetNameKeys = ['road', 'street', 'street_name', 'residential'];
190+
191+
return $this->guessBestComponent($components, $streetNameKeys);
192+
}
193+
194+
/**
195+
* @param array $components
196+
*
197+
* @return null|string
198+
*/
199+
protected function guessSubLocality(array $components)
200+
{
201+
$subLocalityKeys = ['neighbourhood', 'city_district'];
202+
203+
return $this->guessBestComponent($components, $subLocalityKeys);
204+
}
205+
206+
/**
207+
* @param array $components
208+
* @param array $keys
209+
*
210+
* @return null|string
211+
*/
212+
protected function guessBestComponent(array $components, array $keys)
213+
{
214+
foreach ($keys as $key) {
215+
if (isset($components[$key]) && !empty($components[$key])) {
216+
return $components[$key];
217+
}
218+
}
219+
220+
return null;
221+
}
222+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2011 — William Durand <[email protected]>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Geocode Earth Geocoder provider
2+
[![Build Status](https://travis-ci.org/geocoder-php/geocode-earth-provider.svg?branch=master)](http://travis-ci.org/geocoder-php/geocode-earth-provider)
3+
[![Latest Stable Version](https://poser.pugx.org/geocoder-php/geocode-earth-provider/v/stable)](https://packagist.org/packages/geocoder-php/geocode-earth-provider)
4+
[![Total Downloads](https://poser.pugx.org/geocoder-php/geocode-earth-provider/downloads)](https://packagist.org/packages/geocoder-php/geocode-earth-provider)
5+
[![Monthly Downloads](https://poser.pugx.org/geocoder-php/geocode-earth-provider/d/monthly.png)](https://packagist.org/packages/geocoder-php/geocode-earth-provider)
6+
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/geocoder-php/geocode-earth-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/geocode-earth-provider)
7+
[![Quality Score](https://img.shields.io/scrutinizer/g/geocoder-php/geocode-earth-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/geocode-earth-provider)
8+
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
9+
10+
This is the Geocode Earth provider from the PHP Geocoder. This is a **READ ONLY** repository. See the
11+
[main repo](https://github.com/geocoder-php/Geocoder) for information and documentation.
12+
13+
### Install
14+
15+
```bash
16+
composer require geocoder-php/geocode-earth-provider
17+
```
18+
19+
### API Documentation
20+
21+
Geocode Earth uses the Pelias Geocoder under the hood. You can view it's [documentation here](https://github.com/pelias/documentation).
22+
The base API endpoint is https://api.geocode.earth.
23+
24+
### Contribute
25+
26+
Contributions are very welcome! Send a pull request to the [main repository](https://github.com/geocoder-php/Geocoder) or
27+
report any issues you find on the [issue tracker](https://github.com/geocoder-php/Geocoder/issues).
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s:717:"{"geocoding":{"version":"0.2","attribution":"https://geocode.earth/guidelines","query":{"text":"jsajhgsdkfjhsfkjhaldkadjaslgldasd","size":5,"layers":["venue","street","country","macroregion","region","county","localadmin","locality","borough","neighbourhood","continent","empire","dependency","macrocounty","macrohood","microhood","disputed","postalcode","ocean","marinearea"],"private":false,"lang":{"name":"English","iso6391":"en","iso6393":"eng","defaulted":true},"querySize":20,"parser":"addressit","parsed_text":{}},"warnings":["performance optimization: excluding 'address' layer"],"engine":{"name":"Pelias","author":"Mapzen","version":"1.0"},"timestamp":1559249221332},"type":"FeatureCollection","features":[]}";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s:4698:"{"geocoding":{"version":"0.2","attribution":"https://geocode.earth/guidelines","query":{"size":5,"private":false,"point.lat":38.900206,"point.lon":-77.036991,"boundary.circle.lat":38.900206,"boundary.circle.lon":-77.036991,"lang":{"name":"English","iso6391":"en","iso6393":"eng","defaulted":true},"querySize":10},"engine":{"name":"Pelias","author":"Mapzen","version":"1.0"},"timestamp":1559249222407},"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[-77.0372,38.90039]},"properties":{"id":"4140517","gid":"geonames:venue:4140517","layer":"venue","source":"geonames","source_id":"4140517","name":"Slidell House (historical)","confidence":0.8,"distance":0.027,"accuracy":"point","country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"District of Columbia","region_gid":"whosonfirst:region:85688741","region_a":"DC","locality":"Washington","locality_gid":"whosonfirst:locality:85931779","neighbourhood":"Downtown","neighbourhood_gid":"whosonfirst:neighbourhood:85866033","continent":"North America","continent_gid":"whosonfirst:continent:102191575","label":"Slidell House (historical), Washington, DC, USA"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-77.0367,38.9003]},"properties":{"id":"6482379","gid":"geonames:venue:6482379","layer":"venue","source":"geonames","source_id":"6482379","name":"The Hay Adams across from the White House","confidence":0.8,"distance":0.027,"accuracy":"point","country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"District of Columbia","region_gid":"whosonfirst:region:85688741","region_a":"DC","locality":"Washington","locality_gid":"whosonfirst:locality:85931779","neighbourhood":"Downtown","neighbourhood_gid":"whosonfirst:neighbourhood:85866033","continent":"North America","continent_gid":"whosonfirst:continent:102191575","label":"The Hay Adams across from the White House, Washington, DC, USA"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-77.036952,38.900522]},"properties":{"id":"way/55326891","gid":"openstreetmap:address:way/55326891","layer":"address","source":"openstreetmap","source_id":"way/55326891","name":"800 16th Street Northwest","housenumber":"800","street":"16th Street Northwest","postalcode":"20006","confidence":0.8,"distance":0.035,"accuracy":"point","country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"District of Columbia","region_gid":"whosonfirst:region:85688741","region_a":"DC","locality":"Washington","locality_gid":"whosonfirst:locality:85931779","neighbourhood":"Downtown","neighbourhood_gid":"whosonfirst:neighbourhood:85866033","continent":"North America","continent_gid":"whosonfirst:continent:102191575","label":"800 16th Street Northwest, Washington, DC, USA"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-77.036952,38.900522]},"properties":{"id":"way/55326891","gid":"openstreetmap:venue:way/55326891","layer":"venue","source":"openstreetmap","source_id":"way/55326891","name":"Hay-Adams Hotel","housenumber":"800","street":"16th Street Northwest","postalcode":"20006","confidence":0.8,"distance":0.035,"accuracy":"point","country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"District of Columbia","region_gid":"whosonfirst:region:85688741","region_a":"DC","locality":"Washington","locality_gid":"whosonfirst:locality:85931779","neighbourhood":"Downtown","neighbourhood_gid":"whosonfirst:neighbourhood:85866033","continent":"North America","continent_gid":"whosonfirst:continent:102191575","label":"Hay-Adams Hotel, Washington, DC, USA"},"bbox":[-77.0371738,38.9003173,-77.0367231,38.9006934]},{"type":"Feature","geometry":{"type":"Point","coordinates":[-77.036959,38.900533]},"properties":{"id":"us/dc/statewide:2020e495cbdd377f","gid":"openaddresses:address:us/dc/statewide:2020e495cbdd377f","layer":"address","source":"openaddresses","source_id":"us/dc/statewide:2020e495cbdd377f","name":"800 16th Street NW","housenumber":"800","street":"16th Street NW","postalcode":"20006","confidence":0.8,"distance":0.036,"accuracy":"point","country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"District of Columbia","region_gid":"whosonfirst:region:85688741","region_a":"DC","locality":"Washington","locality_gid":"whosonfirst:locality:85931779","neighbourhood":"Downtown","neighbourhood_gid":"whosonfirst:neighbourhood:85866033","continent":"North America","continent_gid":"whosonfirst:continent:102191575","label":"800 16th Street NW, Washington, DC, USA"}}],"bbox":[-77.0372,38.9003,-77.0367,38.9006934]}";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s:2827:"{"geocoding":{"version":"0.2","attribution":"https://geocode.earth/guidelines","query":{"text":"Kalbacher Hauptstraße 10, 60437 Frankfurt, Germany","size":5,"private":false,"lang":{"name":"English","iso6391":"en","iso6393":"eng","defaulted":true},"querySize":20,"parser":"libpostal","parsed_text":{"street":"kalbacher hauptstraße","number":"10","postalcode":"60437","city":"frankfurt","country":"germany"}},"engine":{"name":"Pelias","author":"Mapzen","version":"1.0"},"timestamp":1559249220260},"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[8.636781,50.189017]},"properties":{"id":"de/he/city_of_frankfurtammain:af1f19357b787d33","gid":"openaddresses:address:de/he/city_of_frankfurtammain:af1f19357b787d33","layer":"address","source":"openaddresses","source_id":"de/he/city_of_frankfurtammain:af1f19357b787d33","name":"Kalbacher Hauptstraße 10a","housenumber":"10a","street":"Kalbacher Hauptstraße","postalcode":"60437","confidence":1,"match_type":"exact","accuracy":"point","country":"Germany","country_gid":"whosonfirst:country:85633111","country_a":"DEU","region":"Hessen","region_gid":"whosonfirst:region:85682531","region_a":"HE","macrocounty":"Darmstadt Government Region","macrocounty_gid":"whosonfirst:macrocounty:404227581","county":"Frankfurt","county_gid":"whosonfirst:county:102063589","county_a":"FA","locality":"Frankfurt","locality_gid":"whosonfirst:locality:101913837","neighbourhood":"Römerstadt","neighbourhood_gid":"whosonfirst:neighbourhood:85796311","continent":"Europe","continent_gid":"whosonfirst:continent:102191581","label":"Kalbacher Hauptstraße 10a, Frankfurt, Germany"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[8.636575,50.189044]},"properties":{"id":"de/he/city_of_frankfurtammain:f0bbbc440fb8b4b9","gid":"openaddresses:address:de/he/city_of_frankfurtammain:f0bbbc440fb8b4b9","layer":"address","source":"openaddresses","source_id":"de/he/city_of_frankfurtammain:f0bbbc440fb8b4b9","name":"Kalbacher Hauptstraße 10","housenumber":"10","street":"Kalbacher Hauptstraße","postalcode":"60437","confidence":1,"match_type":"exact","accuracy":"point","country":"Germany","country_gid":"whosonfirst:country:85633111","country_a":"DEU","region":"Hessen","region_gid":"whosonfirst:region:85682531","region_a":"HE","macrocounty":"Darmstadt Government Region","macrocounty_gid":"whosonfirst:macrocounty:404227581","county":"Frankfurt","county_gid":"whosonfirst:county:102063589","county_a":"FA","locality":"Frankfurt","locality_gid":"whosonfirst:locality:101913837","neighbourhood":"Römerstadt","neighbourhood_gid":"whosonfirst:neighbourhood:85796311","continent":"Europe","continent_gid":"whosonfirst:continent:102191581","label":"Kalbacher Hauptstraße 10, Frankfurt, Germany"}}],"bbox":[8.636575,50.189017,8.636781,50.189044]}";

0 commit comments

Comments
 (0)