Okay, I haven’t posted here in a long time, but after much head-smashing-against-the-wall – I’ve done something I’d like to post about.

I’m currently building a web application that uses jQuery and its autocomplete plugin.  While googling around to learn how to use it, I ran across something awesome: geonames.org.  It’s an amazing web service that has craploads of data on geographic places all over the world. Awesome!  They support JSON, so cross-domain requests aren’t an issue.

Rather than use my own, less complete database with an outdated zip code database, I thought “hey, I can use geonames!” — just a few problems:

1) The jQuery autocomplete plugin does not support JSON or JSONP as a data format.  It only accepts this ugly, pipe-delimited format.

2) The autocomplete plugin also has some hard coded defaults it sends on all “remote” queries: “q” and “limit”.  This sucks, because not every remote service uses ?q=foo as a search term.  Geonames does allow q as a search, but it doesn’t behave the way I want. The one that does is called name_startsWith.

So, what do we do?

Here’s my old autoComplete:

$("#city").autocomplete("/admin/autoComplete/getCity.php", {
  selectFirst: true,
  minChars: 1,
  max: 50
 });

My getCity.php searched for all cities in my database containing whatever was sent in the q= parameter, and returned list that jQuery’s autocomplete plugin would be happy with.  The plugin also sends limit=50 – using my max: parameter, and my php handles that.  All fine and good.

However, with a bit of trickery, I was able to make a city autocomplete using the geonames.org service.  Here’s the documentation for the geonames search web service.

First and foremost, the q= parameter searches for an exact match on geonames.org.  We can’t have that.  We need to use their name_startsWith= parameter to search.  This isn’t as nice as a contains type parameter, but it’s all they’ve got (that I know of)…

So, what are we to do?

1) Override q and limit by setting them equal to ” in extraParams.

2) Add the paramter dataType: ‘jsonp’; to our autocomplete.

3) Write a function to parse what geonames.org returns.

What does the code look like?

$("#city").autocomplete("http://ws.geonames.org/searchJSON", {
   dataType: 'jsonp',
   parse: function(data) {
     var rows = new Array();
     data = data.geonames;
     for(var i=0; i<data.length; i++){
       rows[i] = { data:data[i], value:data[i].name, result:data[i].name };
       }
       return rows;
     },
     formatItem: function(row, i, n) {
       return row.name + ', ' + row.adminCode1;
     },
     extraParams: {
       // geonames doesn't support q and limit, which are the autocomplete plugin defaults, so let's blank them out.
       q: '',
       limit: '',
       country: 'US',
       featureClass: 'P',
       style: 'full',
       maxRows: 50,
       name_startsWith: function () { return $("#city").val() }
     },
     max: 50
});

So, what does that mess all mean?

$("#city").autocomplete("http://ws.geonames.org/searchJSON", {

There, we bind an autocomplete to the document element with ID=”city”, and we set it up to call the geonames.org service.

dataType: 'jsonp',

We tell the autocomplete we’re expecting jsonp as our data format – an undocumented feature!

parse: function(data) {
  var rows = new Array();
  data = data.geonames;
  for(var i=0; i<data.length; i++){
    rows[i] = { data:data[i], value:data[i].name, result:data[i].name };
  }
  return rows;
},

We write a function to parse the data we get back, and return it in a way that the autocomplete plugin is happy with.  This function digs into the “geonames” node of the JSON that the service returns, and then spits out data values row by row for the autocomplete.

formatItem: function(row, i, n) {
  return row.name + ', ' + row.adminCode1;
},

This part wasn’t necessary, but I format the item to include the adminCode1 field, which is the state – so my autocomplete will show “Chicago, IL” instead of just “Chicago” — however, the field is still only populated with “Chicago” when I choose that result.

     extraParams: {
       // geonames doesn't support q and limit, which are the autocomplete plugin defaults, so let's blank them out.
       q: '',
       limit: '',
       country: 'US',
       featureClass: 'P',
       style: 'full',
       maxRows: 50,
       name_startsWith: function () { return $("#city").val() }
     },

In the extraParams section, we override q and limit by setting them to nothing. We then set the country code to US – as I only wanted to search for cities in the US.  I also didn’t want other “geographic places besides cities” – such as rivers, etc – so we set featureClass to “P” – which is their code for cities.

Unfortunately, geonames doesn’t return the state (adminCode1 in their jargon) unless you request the “full” format (which returns a LOT more data), so that’s why style: ‘full’ is there.

geonames uses “maxRows” to specify the maximum number of results – I went with 50.

Next, we set name_startsWith – our most important search parameter – to whatever the user has typed into the #city field so far.

     max: 50
});

Finally, we tell the autocomplete plugin to expect a max of 50 rows, and close out our code.

The end result is a search box that uses geonames.ws and finds cities anywhere in the US.

Here’s a demonstration.