Geolocation: Easier Than It Looks

Posted by on November 7, 2011
 

Have you ever wanted to add location-aware content to your web applications? Would you believe me if I told you it was dead easy, and you could be up and running in about 10 minutes?

The first thing you want to do is use someone else’s work. Geolocation is a solved problem; there’s no need to roll your own. I went searching for free Geolocation APIs and found two I wanted to try: MaxMind’s GeoLite API and Quova.

GeoLite

GeoLite is somewhat unique in that their service is free, it doesn’t require registration, and they provide their database for download. That means you host the data on your own site and use their PHP library to make calls to the database.

Here’s how you get GeoLite up and running in your web application.

  • Download the GeoLiteCity database
  • Uncompress the database to a folder that’s readable by your web application
  • Install the PEAR GeoLite library (pear install Net_GeoIP)
  • Code!

The code you’ll need to write is ridiculously minimal. Here’s how I tested my installation.

$geoip = Net_GeoIP::getInstance(dirname(__FILE__) . '/data/GeoLiteCity.dat');
$ipaddress = '72.30.2.43'; // Yahoo!
$location = $geoip->lookupLocation($ipaddress);
var_dump($location);

And here’s the output.

object(Net_GeoIP_Location)[2]
    protected 'aData' =>
        array
        'countryCode' => string 'US' (length=2)
        'countryCode3' => string 'USA' (length=3)
        'countryName' => string 'United States' (length=13)
        'region' => string 'CA' (length=2)
        'city' => string 'Sunnyvale' (length=9)
        'postalCode' => string '94089' (length=5)
        'latitude' => float 37.4249
        'longitude' => float -122.0074
        'areaCode' => int 408
        'dmaCode' => float 807

The Net_GeoIP_Location object makes use of the __get() and __set() magic methods, so retrieving the data from the object is as simple as writing $location->city;.

Quova

Since using GeoLite was so darned easy, I decided to try my hand at another geolocation API. I decided to check out Quova’s offering to see how it compared.

After opening a free developer account and getting my API key, I went to work. Quova’s documentation is excellent, and I was up and running in short order. The PHP example on their site is kind of ugly, in my opinion, so I spent 5 or 10 minutes putting a class together to make the code a little prettier.

Here’s what I came up with.

/**
 * Quova ipinfo API class
 *
 * @category    Example
 * @package     Example_Quova
 * @subpackage  GeoIP
 * @version     $Id$
 */

/**
 * Uses Quova's GeoIP API to get geographical location by IP address
 *
 * To obtain your Quova API key (apikey) and the shared secret
 * that you need to build a digital signature, register your
 * application at http://developer.quova.com/.
 *
 * @category    Example
 * @package     Example_Quova
 * @subpackage  GeoIP
 */
class Example_Quova_GeoIP
{

    /**
     * Quova API key
     *
     * @var string
     */
    private $_apiKey;

    /**
     * Quova shared secret
     *
     * @var string
     */
    private $_secret;

    /**
     * Default URL for Quova's GeoIP service
     *
     * @var string
     */
    private $_defaultService = 'http://api.quova.com/v1/ipinfo/';

    /**
     * Public constructor
     *
     * @param string $apiKey Quova API Key
     * @param string $secret Quova shared secret
     */
    public function __construct($apiKey, $secret)
    {
        $this->_apiKey = $apiKey;
        $this->_secret = $secret;
    }

    /**
     * Get geographical location of IP address from Quova's API
     *
     * @param  string $ipaddress
     * @param  string $format json or XML
     * @return string XML or json, depending on the value of $format
     */
    public function getLocation($ipaddress, $format = 'json')
    {
        $ch = curl_init();

        $parameters = array(
            'apikey' => $this->_apiKey,
            'sig' => $this->generateSig(),
            'format' => $format
        );

        $url = $this->_defaultService
            . $ipaddress
            . '?'
            . http_build_query($parameters);

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $data = curl_exec($ch);
        $headers = curl_getinfo($ch);

        // Check headers here

        curl_close($ch);

        return $data;
    }

    /**
     * Checks headers for response code and issues error message if necessary
     *
     * @param mixed $headers
     */
    public function checkHeaders($headers)
    {
        // this is where I'd test HTTP response codes
    }

    /**
     * Generates sig, an MD5 hash of the API key, the shared secret, and Unix timestamp
     *
     * @return string MD5 hash
     */
    public function generateSig()
    {
        return md5($this->_apiKey . $this->_secret . gmdate('U'));
    }

}

Querying the Quova API is then as simple as this:

$apikey = 'dummyApiKey';
$secret = 'dummySecret';
$ipaddress = '72.30.2.43'; // Yahoo!

$quova = new Example_Quova_GeoIp($apikey, $secret);
$location = json_decode($quova->getLocation($ipaddress));
var_dump($location);

And here’s the output:

object(stdClass)[4]
  public 'ipinfo' =>
    object(stdClass)[5]
      public 'ip_address' => string '72.30.2.43' (length=10)
      public 'ip_type' => string 'Mapped' (length=6)
      public 'Network' =>
        object(stdClass)[6]
          public 'organization' => string 'inktomi corporation' (length=19)
          public 'OrganizationData' =>
            object(stdClass)[7]
              public 'organization_type' => string 'Business Conglomerate' (length=21)
          public 'carrier' => string 'inktomi corporation' (length=19)
          public 'asn' => int 14777
          public 'connection_type' => string 'tx' (length=2)
          public 'line_speed' => string 'high' (length=4)
          public 'ip_routing_type' => string 'fixed' (length=5)
          public 'Domain' =>
            object(stdClass)[8]
              public 'tld' => string 'com' (length=3)
              public 'sld' => string 'yahoo' (length=5)
      public 'Location' =>
        object(stdClass)[9]
          public 'continent' => string 'north america' (length=13)
          public 'latitude' => float 37.33053
          public 'longitude' => float -121.83823
          public 'CountryData' =>
            object(stdClass)[10]
              public 'country' => string 'united states' (length=13)
              public 'country_code' => string 'us' (length=2)
              public 'country_cf' => int 99
          public 'region' => string 'southwest' (length=9)
          public 'StateData' =>
            object(stdClass)[11]
              public 'state' => string 'california' (length=10)
              public 'state_code' => string 'ca' (length=2)
              public 'state_cf' => int 94
          public 'dma' => int 807
          public 'msa' => int 41940
          public 'CityData' =>
            object(stdClass)[12]
              public 'city' => string 'san jose' (length=8)
              public 'postal_code' => string '95122' (length=5)
              public 'time_zone' => int -8
              public 'area_code' => string '408' (length=3)
              public 'city_cf' => int 90

Calling $location->ipinfo->Location->StateData->state_code; gets you the two letter state code. Nice!

Gotchas

As far as gotchas go, there aren’t too many. The big thing to look out for is accuracy. Free geolocation datasets generally promise accuracy withing a range of about 25 miles, and not all datasets match. You’ll notice above that GeoCityLite returns a different city and postal code than Quova for the same IP address. When I tested my own IP address at work (Southaven, MS), GeoCityLite told me the IP address was in Collierville, TN, while Quova said it was in Memphis, TN. Both cities are well within the 25 mile accuracy radius, but neither are in the state of Mississippi. Go figure.

Wrapping Up

Want to add geolocation features to your web application? If you have about 10 minutes to spare, you can. Follow the examples above, and you’ll be up and running in no time. The code you’ll have to write will be minimal, and the return on invested time will be well worth it.


About the author—Jeremy is a PHP developer, a Zend Framework expert, the organizer of Memphis PHP, a public speaker, an open source contributor, and an amateur photographer. He currently resides in his hometown of Memphis, TN.