Geocoding in CakePHP

By Nate (nate)
A couple of weeks ago, I was building an event management system which required that people be able to search events near them. I found several existing solutions, but none that I could integrate as easily as a proper Cake extension.

Introduction


The Geocoded Behavior for CakePHP is the simplest and most powerful way to integrate geocoding into your CakePHP application. It integrates with both Google Maps and Yahoo! Local geocoding services (and is extensible to allow for the inclusion of other services), it automatically caches geocode data, and best of all, it can be implemented in just 3 lines of code.

Downloading & Setup


The Geocoded Behavior is featured in the initial release of the Mashup API Project, which is a new repository for CakePHP components, behaviors and helpers which integrate web service APIs. You can download the latest release of the Mashup API Project here: https://cakeforge.org/frs/?group_id=161
Once you have downloaded and unzipped the release, copy geocoded.php from the /models/behaviors folder into your application's /models/behaviors folder. Then, import /config/sql/geocodes.php into your application's database. Finally, you need an Application ID (Yahoo! Local) or an API key (Google Maps) in order to integrate the service of your choice. You can get your keys here:
  1. Google Maps API Key - http://www.google.com/apis/maps/signup.html
  2. Yahoo! Local App ID - http://search.yahooapis.com/webservices/register_application

A Simple Example


Let's start by verifying that our service works, and we're able to connect to it using our key. We'll create a simple model called Location, using the following SQL:

SQL:

Download code
CREATE TABLE `locations` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `name` varchar(255) default NULL,
  `addr1` varchar(255) default NULL,
  `addr2` varchar(255) default NULL,
  `city` varchar(255) default NULL,
  `state` varchar(255) default NULL,
  `zip` varchar(5) NOT NULL default '',
  `lat` float default NULL,
  `lon` float default NULL,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  PRIMARY KEY  (`id`)
);

Then we'll create our model, designating that we want to enable it for geocoding:

Model Class:

Download code <?php 

class Location extends AppModel {

    var 
$name 'Location';

    var 
$actsAs = array('Geocoded' => array(
        
'key' => 'ABQIAAAAn0kmVahg_WhO0jCT8Z8MkBT2yXp_ZAY8_u....'
    
));
}

?>

Replace the value of 'key' with your Google Map API key or Yahoo! App ID (the default API key for the localhost domain is ABQIAAAAn0kmVahg_WhO0jCT8Z8MkBT2yXp_ZAY8_ufC3CFXhHIE1NvwkxS-Zl837z60cpTjKeSeelhEJVmNOQ).

Then, create a controller that uses the Location model, and add the following to an action:

Controller Class:

Download code <?php 
    pr
($this->Location->geocode('1600 Pennsylvania Ave. Washington DC USA'));
?>

You should then see an array similar to the following:

Download code
Array
(
    [lat] => 38.898758
    [lon] => -77.037691
)

If DEBUG is set to 2 or higher, you should also see the following queries:

SQL:

Download code
    SELECT `Geocode`.`address`, `Geocode`.`lon`, `Geocode`.`lat` FROM `geocodes` AS `Geocode` WHERE `Geocode`.`address` = '1600 pennsylvania ave. washington dc usa' LIMIT 1
    INSERT INTO `geocodes` (`address`,`lat`,`lon`) VALUES ('1600 pennsylvania ave. washington dc usa', 38.898758,-77.037691)

Before querying the web service, the Geocoded behavior checks the cache to see if a lookup on this address has already been performed. Then, after successfully retrieving the results from the web service, it saves those results to the cache table. From now on, any lookups for that address will be read from the cache.

Getting Fancy


Besides strings, the geocode() method will also accept arrays, from which it will attempt to extract address information. This makes it extremely easy to add geo-data to models which include address data. The list of extractable fields is as follows:
Download code
'street', 'address', 'addr', 'address1', 'addr1', 'address2', 'address2', 'apt', 'city', 'state', 'zip', 'zipcode', 'zip_code'

Using beforeSave(), we can automatically save geocoded coordinates to our Location model every time a record is created or updated:

Model Class:

Download code <?php 

class Location extends AppModel {

    var 
$name 'Location';

    var 
$actsAs = array('Geocoded' => array(
        
'key' => 'ABQIAAAAn0kmVahg_WhO0jCT8Z8MkBT2yXp_ZAY8_u....'
    
));

    function 
beforeSave() {
        if (
$coords $this->geocode($this->data)) {
            
$this->set($coords);
        }
        return 
true;
    }
}

?>

This will save any valid coordinate set, based on the data provided. The Location model uses the field combination of 'addr1, 'addr2', 'city', 'state', 'zip' to create the address string. Some other valid combinations are:
  1. street, city, state
  2. address, city, state, zip
  3. addr, apt, city, zipcode
  4. city, zip_code
  5. zipcode
  6. And so on.

Any other valid combination of fields from the list will work just fine. You can also customize the field names in the Location model which are used to store the coordinate data, if, for example you wanted to use the field names 'latitude' and 'longitude'.

The full list of configuration options for the Geocoded behavior is as follows:
  • lookup - The name of the lookup service to use. Currently available options are 'google' and 'yahoo'. Defaults to 'google'.
  • key - The Google Maps API key or Yahoo! Local App ID for your application
  • cacheTable - The name of the table to use when caching geocode data. Defaults to 'geocodes'. Alternatively, you can create a Geocode model which will be used for all saves and lookups, in which case this setting will be ignored.
  • fields - An array containing the field names to use for latitude and longitude data. These should match the field names of your geocoded model. Defaults to array('lat', 'lon').

Searching


After creating a few locations, we can search for ones in our area. The Geocoded Behavior includes a method called findAllByDistance(), which allows you to search for records within a given distance of a certain point. We can first get the coordinates of our search location, then do the search. The findAllByDistance() method can be called in one of two ways:

Download code
findAllByDistance($coords, $distance);
- or -
findAllByDistance($x, $y, $distance);

In the first example, $coords is an array containing longitude and latitude values (in that order). In the second example, $x and $y are longitude and latitude values, respectively. In both examples, $distance is the search radius in miles.

Putting this into practice, we can do something like the following:

Controller Class:

Download code <?php 
    $youAreHere 
$this->Location->geocode("132 Tremont St. Boston, MA");
    
$locations $this->Location->findAllByDistance($youAreHere5);
?>

This will find all the Location records within 5 miles of me. Alternatively, you could create a form based on the address fields in the Location model, and run your searches dynamically:

Controller Class:

Download code <?php 
    
if (!empty($this->data)) {
        
$youAreHere $this->Location->geocode($this->data);
        
$locations $this->Location->findAllByDistance($youAreHere5);
    }
?>

Adding location searching in CakePHP is now as simple as that.

Future versions of the Geocoded Behavior will generate a virtual 'distance' field within your query, allowing you to do sorting and more advanced filtering and comparison. Other future plans include setting a default measurement unit, with automatic unit converstions, as well as setting default array keys from which to generate addresses, as well as methods for facilitating the integration of other geocoding APIs.

Stay tuned for more fun web APIs, and check out the official CakePHP Mashup API Project at https://cakeforge.org/projects/mashup/, where more code examples and API integrations will be appearing shortly.

 

Comments 332

CakePHP Team Comments Author Comments
 

Comment

1 The Simple Example

Hi Nate,

Thanks for the tutorial. I got as far as the simple example and got an error (see below)! It doesnt appear to be creating a full SQL string from what i can tell. Any ideas?

Query: geocode
Warning: SQL Error: 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'geocode' at line 1 in /home/mashupk/public_html/cake/libs/model/datasources/dbo_source.php on line 476
Posted Apr 11, 2007 by kharmer
 

Comment

2 The Simple Example

It looks like you're either not attaching the behavior to the model correctly, or you're not running Cake 1.2. If it's still not working for you, try pasting some code into CakeBin and posting a link.
Posted Apr 11, 2007 by Nate
 

Question

3 The Simple Example

Sorry Nate; I missed the fact that I was meant to be using the 1.2 release of CakePHP. I was using the current stable release - 1.1.14.4797.

Posted Apr 12, 2007 by kharmer
 

Question

4 Using this to Create a View

Thanks for the tutorial. I was wondering how can I use the data to actually embed a map in my webpage with a pointer at the address.
Posted Apr 24, 2007 by Harsh Singh
 

Comment

5 Using this to Create a View

Thanks for the tutorial. I was wondering how can I use the data to actually embed a map in my webpage with a pointer at the address.
That's beyond the scope of this particular tutorial, although a mapping helper will probably be provided in subsequent releases of The Mashup API Project. In the meantime, you can find information on implementing with Google Maps here: http://www.google.com/apis/maps/documentation/
Posted Jun 5, 2007 by Nate
 

Question

6 A possible bugfix and a question

The bugfix:
I added the following to geocoded.php:
function geocode(&$model, $address) {
//go all the way to the end
//added check for failed code lookup
if ($code) {
return array_reverse($code);
} else {
return false;
}
}

it was possible for code to be null if the lookup in the cache passed, but the http lookup failed.

I'm having a problem with lookups to yahoo failing inside:
HttpSocket::decodeChunkedBody - Could not parse malformed chunk. Activate quirks mode to do this. [CORE/cake/libs/http_socket.php, line 399] Context:
$this = httpsocket object
$body = "1e9

44.94141-123.034286
SALEMORUS


0

"
$decodedBody = null
$chunkLength = null
$match = array()

Code:
while ($chunkLength !== 0) {
if (!preg_match("/^([0-9a-f]+)(?:;(.+)=(.+))?\r\n/iU",
$body, $match)) {
if (!$this->quirksMode) {
trigger_error(__('HttpSocket::decodeChunkedBody - Could not parse malformed chunk. Activate quirks mode to do this.', true), E_USER_WARNING);
return false;
}
break;
}
// and so on..
}

so it's triggering this error because something doesn't pass the preg_match in the results from yahoo. google works fine with the same code. I tried overriding quirksMode to no avail. what to do?
Posted Jun 22, 2007 by scook
 

Comment

7 Cake 1.2 Beta

I'm using the most recent CakePHP 1.2 Beta and I'm getting this error:
Warning (512): loadModel is deprecated see App::import('Model', 'ModelName'); [CORE/cake/basics.php, line 1035]
Posted Jan 6, 2008 by Christoph Hochstrasser
 

Comment

8 Works

If you plan to use this in Cake 1.2 Beta you have to replace on line 44 the "loadModel('Geocode')" with "App::import('Geocode')"

have Fun
Christoph Hochstrasser
Posted Jan 6, 2008 by Christoph Hochstrasser
 

Bug

9 Bugfix

I was having problems getting an array with two address fields (I labeled them 'addr1' and 'addr2') to geocode accurately.

Then I noticed (I can't believe I spotted this in passing) a duplicate key in the array on line 77 in geocoded.php:


$vars = array('street', 'address', 'addr', 'address1', 'addr1', 'address2', 'address2', 'apt', 'city', 'state', 'zip', 'zipcode', 'zip_code');

See it? Replace 'address2' with 'addr2'.

Posted Jun 12, 2008 by Adam Friedman
 

Question

10 Geocoding and Pagination

I am fairly new to CakePHP and wonder if there is a way to make pagination work with geocoding. Atm I do not feel comfortable enough to hack behaviours or to write my own ones. I would be glad in case someone has an idea.

I do not forget to thank for this great work.
Posted Jul 31, 2008 by Yves Latour
 

Bug

11 ... and a bug in this comment form script ...

I have tried to post my previous question as "Geocoding and Pagination?" which caused an error saying "This field cannot be left emtpy" or something like it. The question mark seems to have caused this error you may have a look at it and delete this post.
Posted Jul 31, 2008 by Yves Latour
 

Bug

12 Wrong lonlat

I don't understand this at all - I'm getting the wrong coordinates for data I put in!

I've integrated this code into an existing web site, and I'm getting the coordinates with a UK post code like so:

$this->set( 'lonlat', $this->Location->geocode(array('zip' => $user['User']['postcode'])));

Put a post code into Google Maps and you get the right location, but put it through this and it comes out slightly south and ever so slightly to the East. For example:

Postcode: SE22 9QE
Geocoded loc: -0.0673365, 51.4493
Correct loc: -0.068, 51.45996

I get this for a range of other UK post codes that I've tried as well. Any thoughts?
Posted Dec 27, 2008 by Tom Chance
 

Question

13 Update?

We be cool to see some more development on this or at least bring it up to the latest cake revisions.
Posted Jan 6, 2009 by colby guyer
 

Question

14 Sorting Results By Distance

I am using the findAllByDistance() function in one of my projects, but i am having trouble fuguring out how to sort the results by distance, so i can display them closest to furthest. I have looked around forums for the answer, but having been unable to find a solution. Can someone please help with sorting these function results?
Posted Mar 4, 2009 by Robert
 

Comment

15 Question Answered

I am using the findAllByDistance() function in one of my projects, but i am having trouble fuguring out how to sort the results by distance, so i can display them closest to furthest. I have looked around forums for the answer, but having been unable to find a solution. Can someone please help with sorting these function results?
Searching for this answer i came across multiple people who had the same problem i had, so here is the ridiculously easy answer...

Modify the findAll function in the findAllByDistance() function to order by the distance being calculated...

return $model->findAll("(3958 * 3.1415926 * SQRT(({$y2} - {$y}) * ({$y2} - {$y}) + COS({$y2} / 57.29578) * COS({$y} / 57.29578) * ({$x2} - {$x}) * ({$x2} - {$x})) / 180)<= {$distance}", null, "(3958 * 3.1415926 * SQRT(({$y2} - {$y}) * ({$y2} - {$y}) + COS({$y2} / 57.29578) * COS({$y} / 57.29578) * ({$x2} - {$x}) * ({$x2} - {$x})) / 180)", 10);



Posted Mar 5, 2009 by Robert
 

Question

16 Distance in Kilometers

Someone more experienced in math could post how to replace the formula to calculate distances of addresses in kilometers?
tnks all.
Posted Apr 29, 2009 by zzz zzz