Skip to content

Commit 87a0f6d

Browse files
authored
Merge pull request #531 from vicchi/gg-mapzen-provider
Add support for Mapzen's Geocoder
2 parents 0014c0b + bf5d350 commit 87a0f6d

9 files changed

+537
-2
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ providing a powerful abstraction layer for geocoding manipulations.
3030
- [GeoIP2](#geoip2)
3131
- [GoogleMaps](#googlemaps)
3232
- [GoogleMapsBusiness](#googlemapsbusiness)
33+
- [Mapzen](#mapzen)
3334
- [MaxMindBinary](#maxmindbinary)
3435
- [Nominatim](#nominatim)
3536
- [TomTom](#tomtom)
@@ -239,7 +240,8 @@ Chain | `chain` | | | | | meta provider which iterates over a list of providers
239240
[Geonames](http://www.geonames.org/commercial-webservices.html) | `geonames` | yes |no | worldwide | yes | requires registration, no free tier
240241
[Google Maps](https://developers.google.com/maps/documentation/geocoding/) | `google_maps` | yes | supported | worldwide | yes | requires API key. Limit 2500 requests per day
241242
[Google Maps for Business](https://developers.google.com/maps/documentation/business/) | `google_maps_business` | yes | supported | worldwide | yes | requires API key. Limit 100,000 requests per day
242-
[MapQuest](http://developer.mapquest.com/web/products/dev-services/geocoding-ws) | `map_quest` | yes | no | worldwide | yes | both open and [commercial service](http://platform.mapquest.com/geocoding/) require API key
243+
[MapQuest](http://developer.mapquest.com/web/products/dev-services/geocoding-ws) | `map_quest` | yes | no | worldwide | yes | both open and [commercial service](http://platform.mapquest.com/geocoding/) requires API key
244+
[Mapzen](https://mapzen.com/documentation/search/) | `mapzen` | yes | supported | worldwide | yes | requires API key; limited to 6 request/sec, 30,000 request/day
243245
[Nominatim](http://wiki.openstreetmap.org/wiki/Nominatim) | `nominatim` | yes | supported | worldwide | yes | requires a domain name (e.g. local installation)
244246
[OpenCage](http://geocoder.opencagedata.com/) | `opencage` | yes | supported | worldwide | yes | requires API key. 2500 requests/day free
245247
[OpenStreetMap](http://wiki.openstreetmap.org/wiki/Nominatim) | `openstreetmap` | yes | no | worldwide | yes | heavy users (>1q/s) get banned
@@ -296,6 +298,10 @@ $geocoder = new \Geocoder\Provider\GoogleMaps(
296298
A valid `Client ID` is required. The private key is optional. This provider also
297299
supports SSL, and extends the `GoogleMaps` provider.
298300

301+
##### Mapzen
302+
303+
A valid `API key` is required. This provider also supports SSL.
304+
299305
##### MaxMindBinary
300306

301307
This provider requires a data file, and the
@@ -524,7 +530,7 @@ Version `3.x` is the current major stable version of Geocoder.
524530
Cookbook
525531
--------
526532

527-
We have a small cookbook where you can find examples on common use cases:
533+
We have a small cookbook where you can find examples on common use cases:
528534

529535
* [Caching responses](/docs/cookbook/cache.md)
530536
* [Configuring the HTTP client](/docs/cookbook/http-client.md)

phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<!-- <server name="TOMTOM_MAP_KEY" value="YOUR_MAP_KEY" /> -->
2828
<!-- <server name="GOOGLE_GEOCODING_KEY" value="YOUR_GEOCODING_KEY" /> -->
2929
<!-- <server name="OPENCAGE_API_KEY" value="YOUR_GEOCODING_KEY" /> -->
30+
<!-- <server name="MAPZEN_API_KEY" value="YOUR_MAPZEN_API_KEY" /> -->
3031

3132
</php>
3233
<testsuites>

src/Geocoder/Provider/Mapzen.php

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Geocoder package.
5+
* For the full copyright and license information, please view the LICENSE
6+
* file that was distributed with this source code.
7+
*
8+
* @license MIT License
9+
*/
10+
namespace Geocoder\Provider;
11+
12+
use Geocoder\Exception\InvalidCredentials;
13+
use Geocoder\Exception\QuotaExceeded;
14+
use Geocoder\Exception\NoResult;
15+
use Geocoder\Exception\UnsupportedOperation;
16+
use Http\Client\HttpClient;
17+
18+
/**
19+
* @author Gary Gale <[email protected]>
20+
*/
21+
class Mapzen extends AbstractHttpProvider
22+
{
23+
/**
24+
* @var string
25+
*/
26+
const GEOCODE_ENDPOINT_URL = '%s://search.mapzen.com/v1/search?text=%s&key=%s&size=%d';
27+
28+
/**
29+
* @var string
30+
*/
31+
const REVERSE_ENDPOINT_URL = '%s://search.mapzen.com/v1/reverse?point.lat=%f&point.lon=%f&key=%s&size=%d';
32+
33+
/**
34+
* @var string
35+
*/
36+
private $scheme;
37+
38+
/**
39+
* @var string
40+
*/
41+
private $apiKey;
42+
43+
/**
44+
* @param HttpClient $client An HTTP adapter.
45+
* @param string $apiKey An API key.
46+
* @param bool $useSsl Whether to use an SSL connection (optional).
47+
*/
48+
public function __construct(HttpClient $client, $apiKey, $useSSL = true)
49+
{
50+
parent::__construct($client);
51+
52+
$this->apiKey = $apiKey;
53+
$this->scheme = $useSSL ? 'https' : 'http';
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function geocode($address)
60+
{
61+
if (null === $this->apiKey) {
62+
throw new InvalidCredentials('No API Key provided.');
63+
}
64+
65+
// This API doesn't handle IPs
66+
if (filter_var($address, FILTER_VALIDATE_IP)) {
67+
throw new UnsupportedOperation('The Mapzen provider does not support IP addresses, only street addresses.');
68+
}
69+
70+
$query = sprintf(self::GEOCODE_ENDPOINT_URL, $this->scheme, urlencode($address), $this->apiKey, $this->getLimit());
71+
72+
return $this->executeQuery($query);
73+
}
74+
75+
/**
76+
* {@inheritdoc}
77+
*/
78+
public function reverse($latitude, $longitude)
79+
{
80+
if (null === $this->apiKey) {
81+
throw new InvalidCredentials('No API Key provided.');
82+
}
83+
84+
$query = sprintf(self::REVERSE_ENDPOINT_URL, $this->scheme, $latitude, $longitude, $this->apiKey, $this->getLimit());
85+
86+
return $this->executeQuery($query);
87+
}
88+
89+
/**
90+
* {@inheritdoc}
91+
*/
92+
public function getName()
93+
{
94+
return 'mapzen';
95+
}
96+
97+
/**
98+
* @param $query
99+
* @return \Geocoder\Model\AddressCollection
100+
*/
101+
private function executeQuery($query)
102+
{
103+
$request = $this->getMessageFactory()->createRequest('GET', $query);
104+
$content = (string) $this->getHttpClient()->sendRequest($request)->getBody();
105+
106+
if (empty($content)) {
107+
throw new NoResult(sprintf('Could not execute query "%s".', $query));
108+
}
109+
110+
$json = json_decode($content, true);
111+
112+
// See https://mapzen.com/documentation/search/api-keys-rate-limits/
113+
if (isset($json['meta'])) {
114+
switch ($json['meta']['status_code']) {
115+
case 403:
116+
throw new InvalidCredentials('Invalid or missing api key.');
117+
case 429:
118+
throw new QuotaExceeded('Valid request but quota exceeded.');
119+
}
120+
}
121+
122+
if (!isset($json['type']) || $json['type'] !== 'FeatureCollection' || !isset($json['features']) || count($json['features']) === 0) {
123+
throw new NoResult(sprintf('Could not find results for query "%s".', $query));
124+
}
125+
126+
$locations = $json['features'];
127+
128+
if (empty($locations)) {
129+
throw new NoResult(sprintf('Could not find results for query "%s".', $query));
130+
}
131+
132+
$results = [];
133+
foreach ($locations as $location) {
134+
$bounds = [];
135+
if (isset($location['bbox'])) {
136+
$bounds = [
137+
'south' => $location['bbox'][3],
138+
'west' => $location['bbox'][2],
139+
'north' => $location['bbox'][1],
140+
'east' => $location['bbox'][0],
141+
];
142+
}
143+
144+
$props = $location['properties'];
145+
146+
$adminLevels = [];
147+
foreach (['region', 'locality', 'macroregion', 'country'] as $i => $component) {
148+
if (isset($props[$component])) {
149+
$adminLevels[] = ['name' => $props[$component], 'level' => $i + 1];
150+
}
151+
}
152+
153+
$results[] = array_merge($this->getDefaults(), array(
154+
'latitude' => $location['geometry']['coordinates'][1],
155+
'longitude' => $location['geometry']['coordinates'][0],
156+
'bounds' => $bounds ?: [],
157+
'streetNumber' => isset($props['housenumber']) ? $props['housenumber'] : null,
158+
'streetName' => isset($props['street']) ? $props['street'] : null,
159+
'subLocality' => isset($props['neighbourhood']) ? $props['neighbourhood'] : null,
160+
'locality' => isset($props['locality']) ? $props['locality'] : null,
161+
'postalCode' => isset($props['postalcode']) ? $props['postalcode'] : null,
162+
'adminLevels' => $adminLevels,
163+
'country' => isset($props['country']) ? $props['country'] : null,
164+
'countryCode' => isset($props['country_a']) ? strtoupper($props['country_a']) : null
165+
));
166+
}
167+
168+
return $this->returnResults($results);
169+
}
170+
171+
/**
172+
* @param array $components
173+
*
174+
* @return null|string
175+
*/
176+
protected function guessLocality(array $components)
177+
{
178+
$localityKeys = array('city', 'town' , 'village', 'hamlet');
179+
180+
return $this->guessBestComponent($components, $localityKeys);
181+
}
182+
183+
/**
184+
* @param array $components
185+
*
186+
* @return null|string
187+
*/
188+
protected function guessStreetName(array $components)
189+
{
190+
$streetNameKeys = array('road', 'street', 'street_name', 'residential');
191+
192+
return $this->guessBestComponent($components, $streetNameKeys);
193+
}
194+
195+
/**
196+
* @param array $components
197+
*
198+
* @return null|string
199+
*/
200+
protected function guessSubLocality(array $components)
201+
{
202+
$subLocalityKeys = array('neighbourhood', 'city_district');
203+
204+
return $this->guessBestComponent($components, $subLocalityKeys);
205+
}
206+
207+
/**
208+
* @param array $components
209+
* @param array $keys
210+
*
211+
* @return null|string
212+
*/
213+
protected function guessBestComponent(array $components, array $keys)
214+
{
215+
foreach ($keys as $key) {
216+
if (isset($components[$key]) && !empty($components[$key])) {
217+
return $components[$key];
218+
}
219+
}
220+
221+
return null;
222+
}
223+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s:3418:"{"geocoding":{"version":"0.1","attribution":"https://search.mapzen.com/v1/attribution","query":{"text":"Hanover","size":5,"private":false,"querySize":10},"engine":{"name":"Pelias","author":"Mapzen","version":"1.0"},"timestamp":1473682048001},"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[-88.204203,42.027323]},"properties":{"id":"404497623","gid":"whosonfirst:localadmin:404497623","layer":"localadmin","source":"whosonfirst","source_id":"404497623","name":"Hanover","confidence":0.951,"country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"Illinois","region_gid":"whosonfirst:region:85688697","region_a":"IL","county":"Cook County","county_gid":"whosonfirst:county:102084317","localadmin":"Hanover","localadmin_gid":"whosonfirst:localadmin:404497623","label":"Hanover, IL, USA"},"bbox":[-88.263572,41.986227,-88.14458,42.067434]},{"type":"Feature","geometry":{"type":"Point","coordinates":[-78.122906,18.393428]},"properties":{"id":"85672563","gid":"whosonfirst:region:85672563","layer":"region","source":"whosonfirst","source_id":"85672563","name":"Hanover","confidence":0.944,"country":"Jamaica","country_gid":"whosonfirst:country:85632215","country_a":"JAM","region":"Hanover","region_gid":"whosonfirst:region:85672563","label":"Hanover, Jamaica"},"bbox":[-78.3460426069,18.2897883164,-77.9214139468,18.4554710959]},{"type":"Feature","geometry":{"type":"Point","coordinates":[-76.72414,39.19289]},"properties":{"id":"4357340","gid":"geonames:locality:4357340","layer":"locality","source":"geonames","source_id":"4357340","name":"Hanover","confidence":0.734,"country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"Maryland","region_gid":"whosonfirst:region:85688501","region_a":"MD","county":"Howard County","county_gid":"whosonfirst:county:102084263","locality":"Hanover","locality_gid":"whosonfirst:locality:4357340","label":"Hanover, MD, USA"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-70.81199,42.11316]},"properties":{"id":"4938836","gid":"geonames:locality:4938836","layer":"locality","source":"geonames","source_id":"4938836","name":"Hanover","confidence":0.72,"country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"Massachusetts","region_gid":"whosonfirst:region:85688645","region_a":"MA","county":"Plymouth County","county_gid":"whosonfirst:county:102084367","localadmin":"Hanover","localadmin_gid":"whosonfirst:localadmin:404476511","locality":"Hanover","locality_gid":"whosonfirst:locality:4938836","label":"Hanover, MA, USA"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-76.984032,39.81179]},"properties":{"id":"101717245","gid":"whosonfirst:locality:101717245","layer":"locality","source":"whosonfirst","source_id":"101717245","name":"Hanover","confidence":0.718,"country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"Pennsylvania","region_gid":"whosonfirst:region:85688481","region_a":"PA","county":"York County","county_gid":"whosonfirst:county:102080953","localadmin":"Hanover","localadmin_gid":"whosonfirst:localadmin:404487771","locality":"Hanover","locality_gid":"whosonfirst:locality:101717245","label":"Hanover, PA, USA"},"bbox":[-76.999944,39.791156,-76.963061,39.831769]}],"bbox":[-88.263572,18.2897883164,-70.81199,42.11316]}";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s:4780:"{"geocoding":{"version":"0.1","attribution":"https://search.mapzen.com/v1/attribution","query":{"text":"242 Acklam Road, London, United Kingdom","parsed_text":{"name":"242 Acklam Road","number":"242","street":"Acklam Road","regions":["London","United Kingdom"],"admin_parts":"London, United Kingdom"},"size":5,"private":false,"querySize":10},"engine":{"name":"Pelias","author":"Mapzen","version":"1.0"},"timestamp":1473674363289},"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[-0.203602,51.521124]},"properties":{"id":"node:3711723386","gid":"openstreetmap:address:node:3711723386","layer":"address","source":"openstreetmap","source_id":"node:3711723386","name":"240 Acklam Road","housenumber":"240","street":"Acklam Road","postalcode":"W10 5QT","confidence":0.866,"country":"United Kingdom","country_gid":"whosonfirst:country:85633159","country_a":"GBR","macroregion":"England","macroregion_gid":"whosonfirst:macroregion:404227469","region":"Kensington and Chelsea","region_gid":"whosonfirst:region:85683667","locality":"London","locality_gid":"whosonfirst:locality:101750367","neighbourhood":"Westbourne Green","neighbourhood_gid":"whosonfirst:neighbourhood:85864267","label":"240 Acklam Road, London, England, United Kingdom"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-1.282082,54.552668]},"properties":{"id":"node:1485615866","gid":"openstreetmap:address:node:1485615866","layer":"address","source":"openstreetmap","source_id":"node:1485615866","name":"Teesdale Park Acklam Road","housenumber":"Teesdale Park","street":"Acklam Road","postalcode":"TS17 7JU","confidence":0.866,"country":"United Kingdom","country_gid":"whosonfirst:country:85633159","country_a":"GBR","macroregion":"England","macroregion_gid":"whosonfirst:macroregion:404227469","region":"Stockton-on-Tees","region_gid":"whosonfirst:region:85683777","localadmin":"Thornaby","localadmin_gid":"whosonfirst:localadmin:404452219","neighbourhood":"Acklam","neighbourhood_gid":"whosonfirst:neighbourhood:85785205","label":"Teesdale Park Acklam Road, Thornaby, England, United Kingdom"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-70.568928,41.565854]},"properties":{"id":"us/ma/statewide:862559","gid":"openaddresses:address:us/ma/statewide:862559","layer":"address","source":"openaddresses","source_id":"us/ma/statewide:862559","name":"242 Acapesket Road","housenumber":"242","street":"Acapesket Road","postalcode":"02536","confidence":0.647,"country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"Massachusetts","region_gid":"whosonfirst:region:85688645","region_a":"MA","county":"Barnstable County","county_gid":"whosonfirst:county:102084379","localadmin":"Falmouth","localadmin_gid":"whosonfirst:localadmin:404476181","locality":"East Falmouth","locality_gid":"whosonfirst:locality:85950545","neighbourhood":"Kingdom Hall","neighbourhood_gid":"whosonfirst:neighbourhood:85828363","label":"242 Acapesket Road, East Falmouth, MA, USA"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-72.492505,42.704231]},"properties":{"id":"us/ma/statewide:1436753","gid":"openaddresses:address:us/ma/statewide:1436753","layer":"address","source":"openaddresses","source_id":"us/ma/statewide:1436753","name":"242 Old Vernon Road","housenumber":"242","street":"Old Vernon Road","postalcode":"01360","confidence":0.647,"country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"Massachusetts","region_gid":"whosonfirst:region:85688645","region_a":"MA","county":"Franklin County","county_gid":"whosonfirst:county:102084457","localadmin":"Northfield","localadmin_gid":"whosonfirst:localadmin:404475965","neighbourhood":"Satans Kingdom","neighbourhood_gid":"whosonfirst:neighbourhood:420523529","label":"242 Old Vernon Road, Northfield, MA, USA"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-70.528914,41.591705]},"properties":{"id":"us/ma/statewide:2148130","gid":"openaddresses:address:us/ma/statewide:2148130","layer":"address","source":"openaddresses","source_id":"us/ma/statewide:2148130","name":"242 Carriage Shop Road","housenumber":"242","street":"Carriage Shop Road","postalcode":"02536","confidence":0.647,"country":"United States","country_gid":"whosonfirst:country:85633793","country_a":"USA","region":"Massachusetts","region_gid":"whosonfirst:region:85688645","region_a":"MA","county":"Barnstable County","county_gid":"whosonfirst:county:102084379","localadmin":"Falmouth","localadmin_gid":"whosonfirst:localadmin:404476181","neighbourhood":"Kingdom Hall","neighbourhood_gid":"whosonfirst:neighbourhood:85828363","label":"242 Carriage Shop Road, Falmouth, MA, USA"}}],"bbox":[-72.492505,41.565854,-0.203602,54.552668]}";

0 commit comments

Comments
 (0)