Targeted Geolocation with Geonames

Tweet

Location-based applications are all the rage. What used to be prohibitively expensive GPS devices are now sitting in the of pockets of a great many people, embedded within smartphones and other devices on an unprecedented scale, and services making use of location information are springing up all the time. The social web, for example, has become location-aware with existing services such as Facebook following the lead of frontrunners like Foursquare and Gowalla by introducing Places. Twitter has added location information to tweets, more and more services are tailoring themselves to where you are geographically, and of course advertising is getting in on the act with location-based targeting of advertisements a lucrative and expanding area.

In addition to pinpoint accuracy via GPS on handheld devices, other mechanisms from IP address-based geolocation to Wi-Fi triangulation are becoming more and more sophisticated, and with the W3C solidifying support for geolocation with its Geolocation API Specification the opportunities are vast.

In this article I’ll share with you one important aspect of geolocation – mapping a physical location to a descriptive place name. I’ll look at how you can use Geonames, an online service offering free access to a large collection of geographic data, to help you develop applications taking advantage of locative information.

What is Geolocation?

Location-aware applications rely on being able to locate where you are, and this is what geolocation is all about. After all, once the application knows your location, it can go on to find the nearest store, guide you through the appropriate route to a destination, or target relevant advertisements to you. Geolocation, then, is simply the mechanism for identifying your geographical location.

There are two challenges with geolocation – finding out where someone is, and then describing that location. There are a number of techniques for determining someone’s location and they vary in sophistication and, most importantly, in accuracy. Latitude and longitude can pinpoint your location to within meters anywhere on Earth, but the values themselves have little meaning to most people. Sometimes fine resolution isn’t really needed, and it’s sufficient enough to identify the name of the nearest city or town.

The Geonames Service

Geonames is an online resource offering downloadable databases of a raft of geographical information, including mappings of latitude and longitude information to real-world locations. These files are, as you might imagine, huge as well as volatile. You’re free to download and use these files yourself, but there Geonames also makes web services available one of which is what I’ll use here.

In order to use the web services, you’ll need to create a free account. Go to www.geonames.org and click the Login link at the top right of the page. There you’ll be able to sign in or create a new user account. Once you sign up you’ll be sent an email with a confirmation link; check your mail and confirm your account.

After you’ve confirmed your account, click your username at the top right of the page to go to the account management page and enable your account to use the web services.

You now have a username which permits you access to the Geonames’ web services. You may wish to familiarize yourself with the terms and conditions.

The particular service I’ll demonstrate is findNearbyPlaceName, which does just that – finds the nearest place name to a given latitude and longitude. The documentation states that it’s a RESTlike service with data available in XML and JSON formats (if you’re not clear on what a REST service is, I recommend you read up before continuing). The decision whether to use XML or JSON is often a personal one. I prefer JSON because it’s easier to parse, more lightweight, and works beautifully with JavaScript.

A RESTlike Location Web Service

One of the great things about using a REST web service to retrieve information is that, because of its very nature, you can issue a simple GET request in order to test it out – and you can do so using just a web browser! All you need to do is provide a properly formatted URL. In your browser, enter the following URL:

http://api.geonames.org/findNearbyPlaceNameJSON?lat=53.4774&lng=-2.2381&username=yourusername

Let’s break it down:

  • http:// is the transfer protocol. Generally, REST services exploit the ubiquity and ease of HTTP.
  • api.geonames.org is the domain name which becomes the initial segment of the service’s endpoint.
  • findNearbyPlaceNameJSON identifies the service. Often the return format you’re requesting is passed as a parameter, file extension, or even using an ACCEPT header, but this particular service incorporates it in the service’s name.
  • lat=53.4774&lng=-2.2381 are the latitude and longitude parameters for which you are requesting the nearest place name.
  • username=yourusername is the username which you enabled to use the service. This service accepts is as a parameter, but other REST services may implement other authorization schemes such as HTTP-Authentication.

The service responds to the request by sending back the following JSON (I’ve formatted it nicely here for the sake of readability):

{"geonames":[{"countryName":"United Kingdom",
              "adminCode1":"ENG",
              "fclName":"city, village,...",
              "countryCode":"GB",
              "lng":-2.23743438720703,
              "fcodeName":"seat of a second-order administrative division",
              "distance":"0.39678",
              "toponymName":"Manchester",
              "fcl":"P",
              "name":"Manchester",
              "fcode":"PPLA2",
              "geonameId":2643123,
              "lat":53.4809464283615,
              "adminName1":"England",
              "population":395515}]}

As you can see, the result is an object with the property geonames which contains an array of one object with a number of properties. The lat and lng properties provide the geographic location of the place the service has identified, distance shows you how far this place is from the position you provided, and most useful is the name property which tells you the location’s name. countryName property tells you the country, in this case the United Kingdom.

The data also provides information about administrative divisions. The exact nature of such divisions vary from country to country; in this case adminCode1 (the first-level administrative division) identifies “England”; if you were to locate Houston in the United States using this service you’d find that adminCode1 shows “TX” for the state of Texas.

Additionally, geonameId contains the unique identifier used in the Geonames database which, you’ll remember, is downloadable. You may have a use for this at some point if you go on to use the data for something more substantial.

Targeted Prompting – A Practical Example

As an example, let’s assume your web site has feature that prompts the user find cinema listings, and you’d like to personalize the header text with the name of the user’s nearest town – something like, “Find cinema listings near you in Manchester”. Immediately there’s one important limitation you should be aware of: Geolocation is, in the majority of cases, performed on the client. Whether by a web browser or the device itself, this information has to be sent back to the server if you wish to use it there. JavaScript on the client-side would obtain the user’s coordinates and send them to a server-side script via Ajax which then interfaces with the web service and returns a location name.

Server-side Code

Let’s start by writing the server-side PHP code. The Geonames website does provide a PEAR package, as well as libraries for frameworks including Zend Framework and Yii, to simplify access to their services – however, I’ll use plain PHP for the purposes of this tutorial.

This simple class encapsulates the call to the findNearbyPlaceName service:

<?php
class Geonames
{
    protected $username;

    public function  __construct($username) {
        $this->username =  $username;
    }

    public function  getPlaceName($lat, $lng) {
        $url =  sprintf(
            "http://api.geonames.org/findNearbyPlaceNameJSON?lat=%f&lng=%f&username=%s",
            $lat, $lng, $this->username);
        $response = file_get_contents($url);
        if ($response === false) {
            throw new Exception("Failure to obtain data");
        }

        $places = json_decode($response);
        if (!$places) {
            throw new Exception("Invalid JSON response");
        }
        if (is_array($places->geonames) && count($places->geonames)) {
            return $places->geonames[0]->name;
        }
        else {
            return "Unknown";
        }
   }
}

The class is named Geonames, whose constructor takes a single parameter as an argument – the username you registered earlier, which is then stored in a protected member variable.

The getPlaceName() method calls the findNearbyPlaceName service. Because this is a simple GET request to a RESTlike service, you can simply incorporate the parameters in the URL as you did when testing the web service through your browser earlier. I used sprintf() to build the up URL, an approach I feel is cleaner than concatenation and also permits me to specify the format types for the placeholders; latitude and longitude are floats (%f) and the username is a string (%s). The URL is then passed to file_get_contents() which receives the service’s JSON response. The response is decoded using json_decode() and the first place name is returned from the method.

The script called by Ajax will receive the latitudinal and longitudinal coordinates, use them to query the Geonames web service through the new class, and return the name of the nearest location.

<?php
require "../include/Geonames.php";

$lat = floatval($_GET["lat"]);
$lng = floatval($_GET["lng"]);

$geo = new Geonames("username");
$prompt = "Find cinema listings near you";
try {
    $place = $geo->getPlaceName($lat, $lng);
    if ($place != "Unknown") {
        $prompt .= " in " . $place;
    }
}
catch (Exception $e) {
    error_log("Error with web service: " . $e->getMessage());
}
header("Content-Type: text/plain");
echo $prompt;

The code starts with a default message, “Find cinema listings near you”. If the call to the getPlaceName() method doesn’t raise an exception and the name returned isn’t “Unknown”, the message is further personalized.

Client-side Code

Now let’s turn our attention to the HTML page and JavaScript code that will call the PHP.

<!DOCTYPE html>
<html lang="en">
 <head>
  <meta charset="utf-8">
  <title>Hello!</title>
  <script src="jquery.js"></script>
  <script>
(function ($) {
    $(document).ready(function() {
        if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(
                function (position) {
                    var lat = position.coords.latitude;
                    var lng = position.coords.longitude;
                    $("#banner").load("/prompt.php?lat="+lat+"&lng="+lng);
                },
                function (error) {
                    console.log("An error occurred obtaining position.");
                });
        }
        else {
            $("#banner").html("Default content goes here"); 
        }
    });
})(jQuery);
  </script>
 </head>
 <body>
  <div id="banner"></div>
 </body>
</html>

If geolocation is supported by the browser then navigator.geolocation will return true, otherwise some default content is inserted into the element with the ID banner. When geolocation is supported, an attempt is made to obtain the user’s location using navigator.geolocation.getCurrentPosition(), which is passed success and error callbacks.

At this point the browser will ask for your permission to share your location information with the web page. If you grant permission then the success callback is executed and, if the application succeeds in retrieving the location’s name, the banner will contain a personalized invitation to explore the cinema listings. If you deny permission to the browser, the error callback is executed instead.

Summary

In this short article I’ve introduced geolocation and one way in which you can exploit it right now to personalize a visitor’s experience on a website. Although the example I’ve given is somewhat clunky in its use of AJAX to load a block into a page retrospectively, it serves to illustrate one of the technical challenges of geolocation – information is largely obtained client-side and thus there are hurdles you need to overcome, such as asking permission to use the information. Perhaps you may wish to try storing information server-side in a session once you’ve obtained it, look at improving performance using caching, or experiment with more sophisticated targeting. The next logical step is to examine how you would manage relationships between towns – what if my local village doesn’t have a cinema, what’s the nearest town or city that does? That’s for another time, but for now I encourage you to look at API support for geolocation (you could even look at the specification), look at an entirely server-side IP-based solution, or delve into the mathematics behind geolocation. Most of all, I encourage you to have fun!

Image via Olivier Le Moal / Shutterstock

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://www.webpagefavs.com/ Elizabeth

    Very nice however how do you find the longitude and latitude coordinates based upon an IP address for example?

  • http://www.lukaswhite.com Lukas White

    Hi Elizabeth
    There are several possible solutions to finding the latitude and longitude based on an IP address, a number of which use a very similar process to the one described here; that is to say, there are web services you can use along similar lines. For example there is freegeoip.net (http://freegeoip.net/static/index.html). Ultimately the data from this – and a number of other free services – is sourced from a company called Maxmind, who offer free and paid databases and web services. (Web service: http://www.maxmind.com/app/web_services_city_usage, database: http://www.maxmind.com/app/geolitecity, PHP API’s: http://www.maxmind.com/app/php). Hope this helps.

    • megasteve4

      In reference to using ip location tracking sites – the ones I have tried have had limited success. Apparently county is pretty much fine but to drill down to a town is difficult to do with certainty because ISP’s have churning pools of dynamic IP’s that continually change hence can not be pinned to a location. Paid services might be better but I have no experience with them.

  • http://www.rouse.ws William Rouse

    I don’t understand where you place the code starting with:
    <?php
    require "../include/Geonames.php";

    Is this a third file or is this part of the HTML file?
    Thanks!

  • http://www-users.cs.york.ac.uk/~alistair/index.php Alistair Edwards

    ‘locative’?! a grammatical case?

  • http://www.lukaswhite.com Lukas White

    William – sorry if that wasn’t clear, but it’s a separate file called prompt.php in the web root – that’s what gets called in line 15 of the client-side code, via AJAX.

    Alistair – you’re quite right, it’s nothing of the sort. I’ll get that changed.

  • megasteve4

    Nice article!

    I am trying to build a ‘find the nearest’ type search into a web site I am making. My data only has UK post codes. It would appear you can do reversed lookup ups using Geo Names (http://www.geonames.org/export/web-services.html) to get long and lat co-ordinates. What I want to know is does anyone have any experiences using an approach like this in production code – has it worked is the data complete enough? Or would people recommend trying to do the same with google map api?
    Thanks

    • http://www.lukaswhite.com Lukas White

      UK postcodes are a tricky one, thanks to the high cost the Royal Mail place on the data. Depending on the accuracy you’re looking for, though, perhaps outcodes (e.g. SW4, M1) are sufficient? This data is freely available and it’s a manageable amount of data, yet still accurate to a couple of miles.

      Alternatively, freethepostcode.org is a great initiative to try and crowdsource this information and make it freely available, but in my experience it isn’t yet complete enough to use in production. A better alternative might be a commercial online service such as postcodeanywhere.co.uk. I believe Wikileaks published the data once as well.

      The Google Maps API is good, I can certainly recommend that if it seems to fit the bill.

  • http://www.booksmartdiner.co.uk Kevin Moran

    Hi Lukas,
    I have been playing around with your code. If I visit it on my Mac in Safari it works fine but if I visit it on my iPhone 3gs with Safari it doesn’t seem to work.
    Any ideas as I thought this would work on my iPhone?
    Regards
    Kevin