Skip to content

Commit 41a1fd9

Browse files
Extract Generic NominatimProvider
OpenStreetMap uses open source software called Nominatim for their geocoding service, this allows people to use alternative installations of that software
1 parent 23484d2 commit 41a1fd9

File tree

4 files changed

+198
-127
lines changed

4 files changed

+198
-127
lines changed

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ Currently, there are many providers for the following APIs:
3232
* [Google Maps](http://code.google.com/apis/maps/documentation/geocoding/) as Address-Based geocoding and reverse geocoding provider;
3333
* [Google Maps for Business](https://developers.google.com/maps/documentation/business/webservices) as Address-Based geocoding and reverse geocoding provider;
3434
* [Bing Maps](http://msdn.microsoft.com/en-us/library/ff701715.aspx) as Address-Based geocoding and reverse geocoding provider;
35-
* [OpenStreetMaps](http://nominatim.openstreetmap.org/) as Address-Based geocoding and reverse geocoding provider;
35+
* [OpenStreetMaps](http://nominatim.openstreetmap.org/) as Address-Based geocoding and reverse geocoding provider (based on the Nominatim provider);
36+
* [Nominatim](http://wiki.openstreetmap.org/wiki/Nominatim) as Address-Based geocoding and reverse geocoding provider;
3637
* [CloudMade](http://developers.cloudmade.com/projects/show/geocoding-http-api) as Address-Based geocoding and reverse geocoding provider;
3738
* [Geoip](http://php.net/manual/book.geoip.php), the PHP extension, as IP-Based geocoding provider;
3839
* ChainProvider is a special provider that takes a list of providers and iterates
@@ -155,6 +156,11 @@ A valid api key is required.
155156

156157
The `OpenStreetMapsProvider` named `openstreetmaps` is able to geocode and reverse geocode **street addresses**.
157158

159+
### NominatimProvider ###
160+
161+
The `NominatimProvider` named `nominatim` is able to geocode and reverse geocode **street addresses**.
162+
Access to a Nominatim server is required. See the [Nominatim
163+
Wiki Page](http://wiki.openstreetmap.org/wiki/Nominatim) for more information.
158164

159165
### CloudMadeProvider ###
160166

@@ -287,6 +293,9 @@ $geocoder->registerProviders(array(
287293
new \Geocoder\Provider\ArcGISOnlineProvider(
288294
$adapter, $sourceCountry, $useSsl
289295
),
296+
new \Geocoder\Provider\NominatimProvider(
297+
$adapter, 'http://your.nominatim.server', $locale
298+
),
290299
));
291300
```
292301

@@ -298,7 +307,7 @@ Parameters:
298307
* `$service` is available for `MaxMindProvider`.
299308
* `$useSsl` is available for `GoogleMapsProvider`, `GoogleMapsBusinessProvider`, `MaxMindProvider` and `ArcGISOnlineProvider`.
300309
* `$sourceCountry` is available for `ArcGISOnlineProvider`.
301-
310+
* `$rootUrl` is available for `NominatimProvider`.
302311

303312
### Using The ChainProvider ###
304313

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
11+
namespace Geocoder\Provider;
12+
13+
use Geocoder\Exception\NoResultException;
14+
use Geocoder\Exception\UnsupportedException;
15+
use Geocoder\HttpAdapter\HttpAdapterInterface;
16+
17+
/**
18+
* @author Niklas Närhinen <[email protected]>
19+
*/
20+
class NominatimProvider extends AbstractProvider implements LocaleAwareProviderInterface
21+
{
22+
/**
23+
* @param HttpAdapterInterface $adapter An HTTP adapter.
24+
* @param string $rootUrl Root URL of the nominatim server
25+
* @param string $locale A locale (optional).
26+
*/
27+
public function __construct(HttpAdapterInterface $adapter, $rootUrl, $locale = null)
28+
{
29+
parent::__construct($adapter, $locale);
30+
31+
$this->rootUrl = rtrim($rootUrl, '/');
32+
}
33+
34+
/**
35+
* {@inheritDoc}
36+
*/
37+
public function getGeocodedData($address)
38+
{
39+
// This API does not support IPv6
40+
if (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
41+
throw new UnsupportedException('The NominatimProvider does not support IPv6 addresses.');
42+
}
43+
44+
if ('127.0.0.1' === $address) {
45+
return array($this->getLocalhostDefaults());
46+
}
47+
48+
$query = sprintf($this->getGeocodeEndpointUrl(), urlencode($address), $this->getMaxResults());
49+
$content = $this->executeQuery($query);
50+
51+
if (null === $content) {
52+
throw new NoResultException(sprintf('Could not resolve address "%s"', $address));
53+
}
54+
55+
$doc = new \DOMDocument();
56+
if (!@$doc->loadXML($content) || null === $doc->getElementsByTagName('searchresults')->item(0)) {
57+
throw new NoResultException(sprintf('Could not execute query %s', $query));
58+
}
59+
60+
$searchResult = $doc->getElementsByTagName('searchresults')->item(0);
61+
$places = $searchResult->getElementsByTagName('place');
62+
63+
if (null === $places || 0 === $places->length) {
64+
throw new NoResultException(sprintf('Could not execute query %s', $query));
65+
}
66+
67+
$results = array();
68+
69+
foreach ($places as $place) {
70+
$boundsAttr = $place->getAttribute('boundingbox');
71+
list($bounds['south'], $bounds['north'], $bounds['west'], $bounds['east']) = $boundsAttr
72+
? explode(',', $boundsAttr)
73+
: null;
74+
75+
$results[] = array_merge($this->getDefaults(), array(
76+
'latitude' => $place->getAttribute('lat'),
77+
'longitude' => $place->getAttribute('lon'),
78+
'bounds' => $bounds,
79+
'zipcode' => $this->getNodeValue($place->getElementsByTagName('postcode')),
80+
'county' => $this->getNodeValue($place->getElementsByTagName('county')),
81+
'region' => $this->getNodeValue($place->getElementsByTagName('state')),
82+
'streetNumber' => $this->getNodeValue($place->getElementsByTagName('house_number')),
83+
'streetName' => $this->getNodeValue($place->getElementsByTagName('road')) ?: $this->getNodeValue($place->getElementsByTagName('pedestrian')),
84+
'city' => $this->getNodeValue($place->getElementsByTagName('city')),
85+
'cityDistrict' => $this->getNodeValue($place->getElementsByTagName('suburb')),
86+
'country' => $this->getNodeValue($place->getElementsByTagName('country')),
87+
'countryCode' => strtoupper($this->getNodeValue($place->getElementsByTagName('country_code'))),
88+
));
89+
}
90+
91+
return $results;
92+
}
93+
94+
/**
95+
* {@inheritDoc}
96+
*/
97+
public function getReversedData(array $coordinates)
98+
{
99+
$query = sprintf($this->getReverseEndpointUrl(), $coordinates[0], $coordinates[1]);
100+
$content = $this->executeQuery($query);
101+
102+
if (null === $content) {
103+
throw new NoResultException(sprintf('Unable to resolve the coordinates %s', implode(', ', $coordinates)));
104+
}
105+
106+
$doc = new \DOMDocument();
107+
if (!@$doc->loadXML($content) || $doc->getElementsByTagName('error')->length > 0) {
108+
throw new NoResultException(sprintf('Could not resolve coordinates %s', implode(', ', $coordinates)));
109+
}
110+
111+
$searchResult = $doc->getElementsByTagName('reversegeocode')->item(0);
112+
$addressParts = $searchResult->getElementsByTagName('addressparts')->item(0);
113+
$result = $searchResult->getElementsByTagName('result')->item(0);
114+
115+
return array(array_merge($this->getDefaults(), array(
116+
'latitude' => $result->getAttribute('lat'),
117+
'longitude' => $result->getAttribute('lon'),
118+
'zipcode' => $this->getNodeValue($addressParts->getElementsByTagName('postcode')),
119+
'county' => $this->getNodeValue($addressParts->getElementsByTagName('county')),
120+
'region' => $this->getNodeValue($addressParts->getElementsByTagName('state')),
121+
'streetNumber' => $this->getNodeValue($addressParts->getElementsByTagName('house_number')),
122+
'streetName' => $this->getNodeValue($addressParts->getElementsByTagName('road')) ?: $this->getNodeValue($addressParts->getElementsByTagName('pedestrian')),
123+
'city' => $this->getNodeValue($addressParts->getElementsByTagName('city')),
124+
'cityDistrict' => $this->getNodeValue($addressParts->getElementsByTagName('suburb')),
125+
'country' => $this->getNodeValue($addressParts->getElementsByTagName('country')),
126+
'countryCode' => strtoupper($this->getNodeValue($addressParts->getElementsByTagName('country_code'))),
127+
)));
128+
}
129+
130+
/**
131+
* {@inheritDoc}
132+
*/
133+
public function getName()
134+
{
135+
return 'openstreetmaps';
136+
}
137+
138+
/**
139+
* @param string $query
140+
*
141+
* @return string
142+
*/
143+
protected function executeQuery($query)
144+
{
145+
if (null !== $this->getLocale()) {
146+
$query = sprintf('%s&accept-language=%s', $query, $this->getLocale());
147+
}
148+
149+
return $this->getAdapter()->getContent($query);
150+
}
151+
152+
/**
153+
* @return string
154+
*/
155+
protected function getGeocodeEndpointUrl()
156+
{
157+
return $this->rootUrl.'/search?q=%s&format=xml&addressdetails=1&limit=%d';
158+
}
159+
160+
/**
161+
* @return string
162+
*/
163+
protected function getReverseEndpointUrl()
164+
{
165+
return $this->rootUrl.'/reverse?format=xml&lat=%F&lon=%F&addressdetails=1&zoom=18';
166+
}
167+
168+
/**
169+
* @param \DOMNodeList
170+
*
171+
* @return string
172+
*/
173+
private function getNodeValue(\DOMNodeList $element)
174+
{
175+
return $element->length ? $element->item(0)->nodeValue : null;
176+
}
177+
}

src/Geocoder/Provider/OpenStreetMapsProvider.php

Lines changed: 8 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@
1010

1111
namespace Geocoder\Provider;
1212

13-
use Geocoder\Exception\NoResultException;
14-
use Geocoder\Exception\UnsupportedException;
13+
use Geocoder\HttpAdapter\HttpAdapterInterface;
1514

1615
/**
1716
* @author Niklas Närhinen <[email protected]>
1817
*/
19-
class OpenStreetMapsProvider extends AbstractProvider implements LocaleAwareProviderInterface
18+
class OpenStreetMapsProvider extends NominatimProvider
2019
{
2120
/**
2221
* @var string
@@ -29,130 +28,16 @@ class OpenStreetMapsProvider extends AbstractProvider implements LocaleAwareProv
2928
const REVERSE_ENDPOINT_URL = 'http://nominatim.openstreetmap.org/reverse?format=xml&lat=%F&lon=%F&addressdetails=1&zoom=18';
3029

3130
/**
32-
* {@inheritDoc}
33-
*/
34-
public function getGeocodedData($address)
35-
{
36-
// This API does not support IPv6
37-
if (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
38-
throw new UnsupportedException('The OpenStreetMapsProvider does not support IPv6 addresses.');
39-
}
40-
41-
if ('127.0.0.1' === $address) {
42-
return array($this->getLocalhostDefaults());
43-
}
44-
45-
$query = sprintf(self::GEOCODE_ENDPOINT_URL, urlencode($address), $this->getMaxResults());
46-
$content = $this->executeQuery($query);
47-
48-
if (null === $content) {
49-
throw new NoResultException(sprintf('Could not resolve address "%s"', $address));
50-
}
51-
52-
$doc = new \DOMDocument();
53-
if (!@$doc->loadXML($content) || null === $doc->getElementsByTagName('searchresults')->item(0)) {
54-
throw new NoResultException(sprintf('Could not execute query %s', $query));
55-
}
56-
57-
$searchResult = $doc->getElementsByTagName('searchresults')->item(0);
58-
$places = $searchResult->getElementsByTagName('place');
59-
60-
if (null === $places || 0 === $places->length) {
61-
throw new NoResultException(sprintf('Could not execute query %s', $query));
62-
}
63-
64-
$results = array();
65-
66-
foreach ($places as $place) {
67-
$boundsAttr = $place->getAttribute('boundingbox');
68-
list($bounds['south'], $bounds['north'], $bounds['west'], $bounds['east']) = $boundsAttr
69-
? explode(',', $boundsAttr)
70-
: null;
71-
72-
$results[] = array_merge($this->getDefaults(), array(
73-
'latitude' => $place->getAttribute('lat'),
74-
'longitude' => $place->getAttribute('lon'),
75-
'bounds' => $bounds,
76-
'zipcode' => $this->getNodeValue($place->getElementsByTagName('postcode')),
77-
'county' => $this->getNodeValue($place->getElementsByTagName('county')),
78-
'region' => $this->getNodeValue($place->getElementsByTagName('state')),
79-
'streetNumber' => $this->getNodeValue($place->getElementsByTagName('house_number')),
80-
'streetName' => $this->getNodeValue($place->getElementsByTagName('road')) ?: $this->getNodeValue($place->getElementsByTagName('pedestrian')),
81-
'city' => $this->getNodeValue($place->getElementsByTagName('city')),
82-
'cityDistrict' => $this->getNodeValue($place->getElementsByTagName('suburb')),
83-
'country' => $this->getNodeValue($place->getElementsByTagName('country')),
84-
'countryCode' => strtoupper($this->getNodeValue($place->getElementsByTagName('country_code'))),
85-
));
86-
}
87-
88-
return $results;
89-
}
90-
91-
/**
92-
* {@inheritDoc}
93-
*/
94-
public function getReversedData(array $coordinates)
95-
{
96-
$query = sprintf(self::REVERSE_ENDPOINT_URL, $coordinates[0], $coordinates[1]);
97-
$content = $this->executeQuery($query);
98-
99-
if (null === $content) {
100-
throw new NoResultException(sprintf('Unable to resolve the coordinates %s', implode(', ', $coordinates)));
101-
}
102-
103-
$doc = new \DOMDocument();
104-
if (!@$doc->loadXML($content) || $doc->getElementsByTagName('error')->length > 0) {
105-
throw new NoResultException(sprintf('Could not resolve coordinates %s', implode(', ', $coordinates)));
106-
}
107-
108-
$searchResult = $doc->getElementsByTagName('reversegeocode')->item(0);
109-
$addressParts = $searchResult->getElementsByTagName('addressparts')->item(0);
110-
$result = $searchResult->getElementsByTagName('result')->item(0);
111-
112-
return array(array_merge($this->getDefaults(), array(
113-
'latitude' => $result->getAttribute('lat'),
114-
'longitude' => $result->getAttribute('lon'),
115-
'zipcode' => $this->getNodeValue($addressParts->getElementsByTagName('postcode')),
116-
'county' => $this->getNodeValue($addressParts->getElementsByTagName('county')),
117-
'region' => $this->getNodeValue($addressParts->getElementsByTagName('state')),
118-
'streetNumber' => $this->getNodeValue($addressParts->getElementsByTagName('house_number')),
119-
'streetName' => $this->getNodeValue($addressParts->getElementsByTagName('road')) ?: $this->getNodeValue($addressParts->getElementsByTagName('pedestrian')),
120-
'city' => $this->getNodeValue($addressParts->getElementsByTagName('city')),
121-
'cityDistrict' => $this->getNodeValue($addressParts->getElementsByTagName('suburb')),
122-
'country' => $this->getNodeValue($addressParts->getElementsByTagName('country')),
123-
'countryCode' => strtoupper($this->getNodeValue($addressParts->getElementsByTagName('country_code'))),
124-
)));
125-
}
126-
127-
/**
128-
* {@inheritDoc}
129-
*/
130-
public function getName()
131-
{
132-
return 'openstreetmaps';
133-
}
134-
135-
/**
136-
* @param string $query
137-
*
138-
* @return string
31+
* @var string
13932
*/
140-
protected function executeQuery($query)
141-
{
142-
if (null !== $this->getLocale()) {
143-
$query = sprintf('%s&accept-language=%s', $query, $this->getLocale());
144-
}
145-
146-
return $this->getAdapter()->getContent($query);
147-
}
33+
const ROOT_URL = 'http://nominatim.openstreetmap.org';
14834

14935
/**
150-
* @param \DOMNodeList
151-
*
152-
* @return string
36+
* @param HttpAdapterInterface $adapter An HTTP adapter.
37+
* @param string $locale A locale (optional).
15338
*/
154-
private function getNodeValue(\DOMNodeList $element)
39+
public function __construct(HttpAdapterInterface $adapter, $locale = null)
15540
{
156-
return $element->length ? $element->item(0)->nodeValue : null;
41+
parent::__construct($adapter, static::ROOT_URL, $locale);
15742
}
15843
}

tests/Geocoder/Tests/Provider/OpenStreetMapsProviderTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ public function testGetGeocodedDataWithLocalhostIPv4()
338338

339339
/**
340340
* @expectedException \Geocoder\Exception\UnsupportedException
341-
* @expectedExceptionMessage The OpenStreetMapsProvider does not support IPv6 addresses.
341+
* @expectedExceptionMessage The NominatimProvider does not support IPv6 addresses.
342342
*/
343343
public function testGetGeocodedDataWithLocalhostIPv6()
344344
{
@@ -420,7 +420,7 @@ public function testGetGeocodedDataWithRealIPv4WithLocale()
420420

421421
/**
422422
* @expectedException \Geocoder\Exception\UnsupportedException
423-
* @expectedExceptionMessage The OpenStreetMapsProvider does not support IPv6 addresses.
423+
* @expectedExceptionMessage The NominatimProvider does not support IPv6 addresses.
424424
*/
425425
public function testGetGeocodedDataWithRealIPv6()
426426
{

0 commit comments

Comments
 (0)