PHP
Article

Where are you? Implementing geolocation with Geocoder PHP

By Arno Slatius

The beauty of SitePoint, to me, is that you can get inspired to try something or be told about some cool project out there. The internet is simply too big for one person to scout out on their own. Geocoder was one of those for me. I had never heard about it and came across it on the authors Trello board.

I love working with maps and geographic information and I use (reverse) geocoding heavily for a project I did for a client; CableTracks. We actually use a paid service for this although not for everything. The paid results hold much more information than you get from free services. I found out that Geocoder PHP actually is what I was missing for the integration of various services that we use.

Geocoder PHP provides: “an abstraction layer for geocoding manipulations”. The library is split into three parts: an HttpAdapter for doing requests, several geocoding Providers and various Formatter/Dumpers to do output formatting.

Installation

Installation of Geocoder is most easily done using composer. Add the following to your composer.json:

{
    "require": {
        "willdurand/geocoder": "@stable"
    }
}

Or get one of the archives from the Geocoder PHP website.

GeoCoding

We’ll first take a look at geocoding adresses. The input for this is usually a street address, but can also be a neighborhood, area, place, state or country. The geocoded output should have a good pointer to where on the globe you should look for what you used as input. The result, of course, depends on the quality of the used geocoder.

To use Geocoder you’ll first need an HttpAdapter to fire the requests at the web service. The HttpAdaper is a web client that actually comes in 5 different flavors with PhpGeocoder. You can use the always available CUrl or Socket based clients. Additionally you can add PHP clients like Buzz Browser, Guzzle PHP HTTP Client, Zend HTTP and the GeoIP2 webservice API. You’ll need to add those to your project yourself if you want to use them.

With the HTTP adapter you can then use a geocoder to do the work. The list of supported geocoders is long, very long. They can be divided into a two groups;

Most can be used for free and some require you to register for an API-key. Most will have some sort of rate or daily limit on the amount of queries that you can shoot their way. Google, for instance, will allow you to do 2500 requests/day.
The quality of the geocoding services varies. Some support specific countries, and usually will be better for those countries. But this is not what we’ll be evaluating here. It is something you do want to test out for yourself, because results will depend on the place you’re geocoding.

Enough about all the possibilities, let’s get to work. We’ll start with a simple example first.

$lookfor = 'Laan van Meerdervoort, Den Haag, Nederland';

$adapter  = new \Geocoder\HttpAdapter\CurlHttpAdapter();
$geocoder = new \Geocoder\Geocoder();
$geocoder->registerProvider(new \Geocoder\Provider\GoogleMapsProvider($adapter));

$result = $geocoder->geocode($lookfor);

Simple, right? This is what you’ll get as result:

Geocoder\Result\Geocoded#1
(
    [*:latitude] => 52.0739343
    [*:longitude] => 4.2636776
    [*:bounds] => array
    (
        'south' => 52.0610866
        'west' => 4.2262026
        'north' => 52.0868446
        'east' => 4.3008719
    )
    [*:streetNumber] => null
    [*:streetName] => 'Laan Van Meerdervoort'
    [*:cityDistrict] => null
    [*:city] => 'The Hague'
    [*:zipcode] => null
    [*:county] => 'The Hague'
    [*:countyCode] => 'THE HAGUE'
    [*:region] => 'South Holland'
    [*:regionCode] => 'ZH'
    [*:country] => 'The Netherlands'
    [*:countryCode] => 'NL'
    [*:timezone] => null
)

This is actually the most beautiful part of Geocoder PHP; which ever geocoder you use, what ever direction you’re geocoding in (normal/reverse), you’ll always get the same result set. That’s invaluable if you’d ever think about changing the used geocoder.

About my address choice; the Laan van Meerdervoort is one of the longest streets in the Netherlands (~5800m long). I left out a house number to get the full street. The difference in the latitude/longitude and the bounds make this obvious now. These bounds, in this case, hold the bounding box of the entire street. It’s very practical that you get both in your result; the center of the street and the bounds.

Most geocoders can be asked to be locale or region sensitive and Geocoder has this nicely integrated with a LocaleAwareProviderInterface. Say you’re Russian, for instance, and you need to go to the Peace Palace which is on the above street you’d use it like this:

$geocoder->registerProvider(new \Geocoder\Provider\GoogleMapsProvider($adapter, 'Ru'));
$result = $geocoder->geocode('Laan van Medevoort 7, Den Haag, Nederland');

And get:

Geocoder\Result\Geocoded#1
(
  [*:latitude] => 52.0739343
  [*:longitude] => 4.2636776
  [*:bounds] => array
  (
    'south' => 52.0610866
    'west' => 4.2262026
    'north' => 52.0868446
    'east' => 4.3008719
  )
  [*:streetNumber] => 7
  [*:streetName] => 'Laan Van Meerdervoort'
  [*:cityDistrict] => null
  [*:city] => 'Гаага'
  [*:zipcode] => null
  [*:county] => 'Гаага'
  [*:countyCode] => 'ГААГА'
  [*:region] => 'Южная Голландия'
  [*:regionCode] => 'ZH'
  [*:country] => 'Нидерланды'
  [*:countryCode] => 'NL'
  [*:timezone] => null
)

Of course you want to get the best results out of your geocoding. If you’re in the US you might want to use a coder that specifically supports the US and another for outside the US. The beauty is that Geocoder supports the chaining of geocoders. You can then throw your request at a set of coders and the first valid result will be returned. This is great because otherwise you’d have to try/catch a whole set of coders until you got a result.
An example of this:

$adapter  = new \Geocoder\HttpAdapter\CurlHttpAdapter();
$geocoder = new \Geocoder\Geocoder();
$chain    = new \Geocoder\Provider\ChainProvider([
  new \Geocoder\Provider\OpenStreetMapProvider($adapter),
  new \Geocoder\Provider\GoogleMapsProvider($adapter),
  new \Geocoder\Provider\BingMapsProvider($adapter, $bingApiKey),
]);
$geocoder->registerProvider($chain);

Do note that Geocoder will return the first valid result. That might not be the best you can get! So take good care in the sequence you chain your coders together!

Reverse geocoding

Reverse geocoding is when you’ve got a point on the globe, usually a GPS (WGS84) coordinate. If your input is in another projection you could use the PHP version of Proj4 to transform it.
The syntax, once you’ve got an instance of a geocoder, again is quite simple. The only thing you’ll need to remember is that it’s latitude first, then longitude (hands up if you also keep switching latitude and longitude accidentally…).
Make sure your geocoder supports reverse geocoding because not all do. I’ll geocode the geographic center of the Netherlands:

$result = $geocoder->reverse(52.155247, 5.387452);

Which leads to a geocoded result like this.

Geocoder\Result\Geocoded#1
(
  [*:latitude] => 52.1551992
  [*:longitude] => 5.3872474
  [*:bounds] => array
  (
    'south' => 52.1551992
    'west' => 5.3872474
    'north' => 52.1551992
    'east' => 5.3872474
  )
  [*:streetNumber] => '30'
  [*:streetName] => 'Krankeledenstraat'
  [*:cityDistrict] => 'Stadskern'
  [*:city] => 'Amersfoort'
  [*:zipcode] => '3811 BN'
  [*:county] => 'Amersfoort'
  [*:countyCode] => 'AMERSFOORT'
  [*:region] => 'Utrecht'
  [*:regionCode] => 'UT'
  [*:country] => 'The Netherlands'
  [*:countryCode] => 'NL'
  [*:timezone] => null
)

Geocoding IP addresses

I’ve personally used geocoding on IP addresses to determine what language to show a website in (combined with browser accepts of course). You feed the IP address of your user, usually from $_SERVER['REMOTE_ADDR'], to the geocoder and get a result. Something to consider is whether or not your server supports IPv6 and is able to serve over IPv6. If so, then you’ll need to use a service that supports geocoding IPv6 because not all do.

There are several IP geocoders available, and using them is as simple as using the others:

$adapter  = new \Geocoder\HttpAdapter\CurlHttpAdapter();
$geocoder = new \Geocoder\Geocoder();
$result   = $geocoder->registerProvider( new \Geocoder\Provider\FreeGeoIpProvider($adapter) )
                     ->geocode($_SERVER['REMOTE_ADDR']);

The result, again, is a Geocoded result. I won’t show an example because you’ll be disappointed. IP geocoders results vary a lot in quality. Most often you’ll only get a country and nothing else. I’ve set up some examples for Geocoding IP addresses if you’re interesed in the results for your IP.

Another thing to note in the last example is that you should always nest your geocoding attempts with Geocoder PHP in a try/catch block. The geocoder is able to throw several exceptions at you depending on the output of the geocoder you’re using. Most are quite obvious but can be handled nicely by adding several catch conditions:

try {
    $result = $geocoder->registerProvider( new \Geocoder\Provider\FreeGeoIpProvider($adapter) )
                       ->geocode($_SERVER['REMOTE_ADDR']);
} catch (NoResultException $e) {
    $result = "No result was returned by the geocoder";
} catch (QuotaExceededException $e) {
    $result = "We met our daily quota";
} catch (Exception $e) {
    $result = "Error: " . $e->getMessage();
}

Output formatting

The last thing I want to touch on is the output formatting. Geocoder has several output formatters that allow you to easily integrate its result into your application. There are two types of output; a string Formatter that has a sprintf-like interface and Dumpers which allow for a language specific output.

The supported output dumpers are XML like GPS eXchange Format (GPX), Keyhole Markup Language (KML) for interfacing with Google Earth, Well-Known Binary (WKB) and Well-Known Text (WKT) for your geospatial enabled databases and finally GeoJSON for easy JavaScript interfacing.

For example; get a geocoded result and feed that to a formatter or dumper;

$adapter  = new \Geocoder\HttpAdapter\CurlHttpAdapter();
$GeoJSON  = new \Geocoder\Dumper\GeoJsonDumper;
$geocoder = new \Geocoder\Geocoder();
$geocoder->registerProvider( new \Geocoder\Provider\OpenStreetMapProvider($adapter) );
echo $GeoJSON->dump( $geocoder->reverse(52.155247, 5.387452) );

This will output a GeoJSON result which is quite usable with OpenLayers for instance:

{
    "type": "Feature",
    "geometry": {
        "type": "Point",
        "coordinates": [
            5.3872095538856,
            52.1551691
        ]
    },
    "properties": {
        "streetName": "Lieve Vrouwekerkhof",
        "zipcode": "3811 AA",
        "city": "Amersfoort",
        "cityDistrict": "Amersfoort",
        "region": "Utrecht",
        "country": "Nederland",
        "countryCode": "NL"
    }
}

Conclusion

Compliments have to be made about the great class structure in Geocoder. It is a true OOPHP library that makes good use of namespaces and interfaces. It is extremely easy to use. Extending it or building your own Geocoders for it should be very easy.

If straightforward geocoding is what you are looking for, then this library will work great for you. I personally see room for an even better Geocoder because there is more power available. It would be great if you were able to set conditions on the request you’re making to a geocoding chain to get the best result available from all coders. For instance: “at least return a street name” in the result set or “have a confidence level of > 75%” (some geocoders supply this information) or a “maximum address error of less than 500 meters” (something that could be calculated taking the difference of the output address and the input coordinates). Of course all this depends on individual geocoders’ possibilities, but simple filtering can certainly be done.

If you’re interested in the geocoders themselves; I’ve set up a small demo to check out the results for various geocoders.

Did you ever work with geocoders, needed a library like this or perhaps work with it already? Please share your thoughts with us.

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.