12. Jun. 2012

Geocoding with CakePHP

12 Jun

Geocoding is a very powerful tool if you want to work with lat/lng, distances or just want to display a small map for your addresses. I use it in combination with my GoogleMaps Helper to display a dynamic or a static map with those addresses (as small pins on the map) or to display how far away you currently are (in km or miles) from this point.

The full package consists of a Lib, a Behavior and their tests and is in my Tools plugin.

Basics first: The lib

I moved the actual querying to a lib for flexibility and better unit testing. It can be used standalone from anywhere in your app where you want to geocode an address (get lat/lng) or reverse-geocode coordinates etc.


App::uses('GeocodeLib', 'Tools.Lib');
$this->Geocode = new GeocodeLib();
$this->Geocode->setOptions(array('host' => 'de')); //optional - you can set it to your country's top level domain.
if ($this->Geocode->geocode('12345 Cityname', $settings)) {
	$result = $this->Geocode->getResult();

Thats all there is to it. You get a result array. Use debug() to display it if you are unsure what it contains.

Some of the available setting options are:

  • min_accuracy (see below – behavior)
  • allow_inconlusive
  • log (for debugging)
  • address (array of fields to map)
  • expect
  • overwrite (see below – behavior
  • before (validate/save)

For details see the class or the test cases (which do not cover all yet, though – feel free to help out).

Reverse geocoding

if ($this->Geocode->reverseGeocode($lat, $lng, $settings)) {
	$result = $this->Geocode->getResult();

will retrieve a matching address to your coordinates.

There are also some helper methods included already:


$result = $this->Geocode->distance($pointOne, $pointTwo); // array('lat' => ..., 'lng' => ...);

returns the distance between those two points.


$result = $this->Geocode->convert($value, $fromUnit, $toUnit);

helps to convert miles to km etc.


$coordinate = GeocodeLib::blur($coordinate, $level); // level=1...5

Helpful when saving geocoded user data and displaying them publicly (google map etc).
It would be dangerous to display the exact coordinates (if they entered their street name and number) of other users. In such a case you should modify the coordinates prior to displaying them. The above code snippet can be used in the view before you pass it on to the GoogleMap helper or before you print it out. The higher the level the more blurred the coordinate.

The Behavior for (automatic) geocoding on the fly

So if we have an address model, all we need to do is adding three fields: lat, lng (both float 9,4 for example) and formatted_address (optional if you want to capture/store the full address string).
Those will be filled by the behavior if we attach our behavior like so:

public $actsAs = array('Tools.Geocoder' => array('real' => false, 'override' => true));

Override means it always updates the coordinates if the address is provided. Real means the fields have to actually exist in the table – like street, postal_code and city etc.

There are many more options – please take a look at the behavior documentation in the class itself or the test cases for details.
One important configuration option is min_accuracy:

'min_accuracy' => GeocodeLib::ACC_SUBLOC

means that the geocoding response is only valid if we can find an address as exact as a sub-locality or better. This can help in validation addresses since incorrect addresses would fall back to pretty unspecific results (and therefore bad accuracy).

Advanced tips

If you have a country Model related to your addresses you probably want to include the country in your query (otherwise it might find the address in some other country).
Just passing the country_id will not do the trick, of course. You should manually add the field country to your data before passing it on to the model:

$this->request->data['Address']['country'] = $countryName; // as a string
 if ($this->Address->save($this->request->data) {}

My plan was to automatically extract the country name in the behavior if country_id is passed – but that might be a little bit too much magic.


This is a little bit more complicated. Basically, if we want to retrieve results based on distance we need to pass one point (lat/lng) to the behavior and calculate the distance in the database at runtime. Since 2.x it is very easy to use virtual fields for this.
The behavior has support for the virtual fields needed to do this using setDistanceAsVirtualField():

$this->Address->setDistanceAsVirtualField($lat, $lng);
$options = array('order' => array('Address.distance' => 'ASC'));
$res = $this->Address->find('all', $options);

Using this in pagination is supported, as well.

The default unit is GeocodeLib::UNIT_KM but can easily be switched to miles, nautical miles, …

Note: If you do not have lat/lng fields in your table yet, you need to add them first. Then run a loop for all records using save() to geocode the address etc. After this you will be ready to use filtering here. You cannot geocode at runtime. It is too costly. So do it once on save and then you can use the lat/lng fields as often as you want.


The behavior adds two new rules you can use: validateLatitude and validateLongitude.
See the test cases for details on how to use it.

Dynamically adding them works as well, of course:

$this->Address->validator()->add('lat', 'validateLatitude', array('rule'=>'validateLatitude', 'message'=>'validateLatitudeError'));
$this->Address->validator()->add('lng', 'validateLongitude', array('rule'=>'validateLongitude', 'message'=>'validateLongitudeError'));
$data = array(
	'lat' => 44,
	'lng' => 190,
$res = $this->Address->validates(); // will return false in this case


I want to cleanup and rewrite some of it – as soon as the test cases are complete.
Not just because this code is mainly from 1.2 and only "upgraded", but also because throwing Exceptions and improving the overall workflow would probably be a good idea.

4.33 avg. rating (86% score) - 3 votes

Posted by Mark in CakePHP


Tags: , , , ,

Leave a Reply

If you need to post a piece of code use {code type=php}...{/code}.
Allowed types are "php", "mysql", "html", "js", "css".

Please do not escape your post (leave all ", <, > and & as they are!). If you have encoded characters and need to reverse ("decode") it, you can do that here!

  1. Karl

    December 3, 2012 at 20:35

    Thanks for the help Mark! I did notice however, sometimes

    if ($this->Geocode->geocode('12345 Cityname', $settings)) {
    $result = $this->Geocode->getResult();

    The if statement returns true, but $result contains nither lat or lng!

  2. Chirayu

    February 6, 2013 at 10:11

    I didnt get how to set the zoom level. I tried hard but always got defaultZoom=5 which is set default in the helper class. Can I over ride this value? I want to set custom zoom level.

  3. Mark

    February 6, 2013 at 13:11

    Try to look into the source code and/or the test cases. Should usually give you a pretty good example of what is going on 🙂

    $options = array(
    echo $this->GoogleMapV3->map($options);
  4. Chirayu

    February 6, 2013 at 14:22

    Yes I did try what you suggested here after having a look on the source code of V3 but no useful conclusion.

  5. Chirayu

    February 6, 2013 at 14:24

    Infact any over riding of defaultOption is not working in my case except ‘div’.

  6. Mark

    February 6, 2013 at 14:43

    I am pretty sure this should work:

    $this->GoogleMapV3->setControls(array('zoom' => 6));

    Also, you could set the zoom globally using Configure::write(&#039;Google.zoom&#039;, 6); in your configs/bootstrap.

  7. Chirayu

    February 8, 2013 at 12:28

    Use of undefined constant WINDOWS – assumed ‘WINDOWS’ [APP\Plugin\tools\Lib\HttpSocketLib.php, line 186]

    while calling "$this-&gt;Geocode-&gt;geocode($location, $settings=array())" to get Lat, Long for the location.

  8. Chirayu

    February 8, 2013 at 13:04

    This is resolved:
    I didnt add bootstrap of plugin to main bootstrap file

  9. Kent

    February 22, 2013 at 21:51

    I updated from git on 2013-02-22T12:46:00-08:00.

    I am getting the following error in CakePhp:

    Notice (8): Use of undefined constant WINDOWS - assumed 'WINDOWS' [APP/Plugin/Tools/Lib/HttpSocketLib.php, line 186]

    From this code segment:

    // Geocode Client adress
    $this->Geocode = new GeocodeLib();
    $geocodeSettings = array ( 'allow_inconlusive' => true );
    if ($this->Geocode->geocode($googleMapAddressString, $geocodeSettings)) {
        $googleGeocodeClient = $this->Geocode->getResult();
    error_log("googleGeocodeClient".print_r($googleGeocodeClient, true));

    The error log show a good decoding:

    [22-Feb-2013 20:44:53 UTC] googleGeocodeClientArray
        [formatted_address] => 277 Edwards Road, Monmouth, Oregon 97361, Vereinigte Staaten
        [type] => street_address
        [country] => USA
        [country_code] => US
        [country_province] => Oregon
        [country_province_code] => OR
        [postal_code] => 97361
        [locality] => Monmouth
        [sublocality] => 
        [route] => Edwards Road 277
        [lat] => 44.8465844
        [lng] => -123.2188478
        [location_type] => RANGE_INTERPOLATED
        [viewport] => Array
                [sw] => Array
                        [lat] => 44.8452354
                        [lng] => -123.2202060
                [ne] => Array
                        [lat] => 44.8479333
                        [lng] => -123.2175080
        [bounds] => Array
                [sw] => Array
                        [lat] => 44.8465843
                        [lng] => -123.2188662
                [ne] => Array
                        [lat] => 44.8465844
                        [lng] => -123.2188478
  10. Mark

    February 23, 2013 at 17:02

    You just didnt include the bootstrap as explained in the readme (see the plugin readme @ github)

  11. Kent

    February 24, 2013 at 00:27

    Yep. Just means I can’t read 😉 I had the CakePlugin::load(‘Tools’); but not the App::import(‘Lib’, ‘Tools.Bootstrap/MyBootstrap’);

  12. Deon (Cape Town)

    March 18, 2013 at 21:54

    Thanks, you saved me a couple of hours.

  13. Derek

    August 6, 2013 at 17:05

    Great plugin! Saved me a lot of time.

    Marker animations can easily be added with a small update to the GoogleMapV3Helper:

    diff --git a/View/Helper/GoogleMapV3Helper.php b/View/Helper/GoogleMapV3Helper.php
    index 1c79dbe..c18dd47 100644
    --- a/View/Helper/GoogleMapV3Helper.php
    +++ b/View/Helper/GoogleMapV3Helper.php
    @@ -168,6 +168,7 @@ class GoogleMapV3Helper extends AppHelper {
                            //'autoCenter' => true,
    +      'animation' => null,
                            'icon' => null, # => default (red marker) //
                            'title' => null,
                            'shadow' => null,
    @@ -525,6 +526,10 @@ class GoogleMapV3Helper extends AppHelper {
                            $params['zIndex'] = $options['zIndex'];
    +    if (isset($options['animation'])) {
    +      $params['animation'] = 'google.maps.Animation.'.$options['animation'];
    +    }
                    // geocode if necessary
                    if (!isset($options['lat']) || !isset($options['lng'])) {
                            $this->map.= "

    Then when creating you’re map:

        'lat' => $result['lattitude'],
        'lng' => $result['longitude'],
        'title' => $result['name'],
        'animation' => 'DROP'
  14. Mark

    August 6, 2013 at 17:12

    Derek, are you able to PR those changes to my github repo? That would be awesome!

  15. Arjan

    February 14, 2014 at 06:47

    Hi! Nice piece of work.

    I’m having a little issue with the geocode behaviour, this is my code:

    public $actsAs = array('Tools.Geocoder' => array(
    		'real' => false, 
    		'override' => true,
    		'language' => 'en',
    		'address' => 'recaddress',
    		'min_accuracy' => GeocodeLib::ACC_STREET,

    The behaviour works fine, I get all data. However when I try to find an address in Greece, Cyprus or Bulgaria the formatted address will be in Greek or Cyrillic.

    When I manually use the geocode function on the same address I do get the formatted address in English.

    I have tried several language settings (de, en, nl or even empty). Manually it all works fine, in the behaviour I’m unable to get the formatted address in Latin letters.

  16. Boris

    January 13, 2015 at 12:42

    Hello Mark,

    following this tutorial here, I get an error message like:

    {code type=php} Error: Unsupported operand types
    File: C:\Program Files (x86)\Ampps\www\kklage\Plugin\Tools\Lib\GeocodeLib.php
    Line: 244{/quote}

    Do you have any idea, what this can be? Thx in advance!

  17. Mark

    January 13, 2015 at 13:34

    Are you sure the 2nd param is an array? You should also post your code in order for me to help you.
    But I am also pretty sure that this is a developer’s error.

  18. Boris

    January 13, 2015 at 14:04

    Sorry to bother you,

    my Controller’s code is

    class ArbeitgeberController extends AppController {
          public function index() 
        App::uses('GeocodeLib', 'Tools.Lib');
    	$this->Geocode = new GeocodeLib();
    	$this->Geocode->setOptions(array('host' => 'de')); //optional - you can set it to your country's top level domain.
    	if ($this->Geocode->geocode('12345 Cityname', $settings)) {
        $result = $this->Geocode->getResult();

    In the view I have:

    I am a bloody newbe to cakephp, but I really like, what I see, so far. Could you please be so nice to help me out with this?

  19. Mark

    January 13, 2015 at 14:08

    You need to at least understand basic PHP.
    Where is $settings defined in your case? The example just contains it for completeness.

  20. Boris

    January 13, 2015 at 14:43

    Thank you very much! I got it to run, eventually. BTW, I assume you are living in Germany. Is there a way to contact you for business?

  21. Jure

    August 5, 2015 at 19:14

    Hi Mark,

    I am just playing around with your geocoder.

    In my controller I have this:

    public function search()
            $this->layout = 'front';
            if (isset($this->request->data['search'])) {
            	// ...
            	$query = $this->request->data['search'];
            	$this->Geocode = new Geocode();
            	$geo = $this->Geocode->geocode($query);
                    $result = $this->Geocode->getResult();
                    if ($result['valid_results'] > 0) {
                    	$options = array('lat' => $result['lat'], 'lng' => $result['lng']);
                        $res = $this->Shops->find()->find('distance', $options)->find('all')->toArray();
                    	$this->set('shops', $res);
            	$this->set('shop', 'fail');

    How can I get shops + the distance. I only get data with the correct distance. How can I achieve this ?

    I am using cake3

    Best regards.

  22. Mark

    August 5, 2015 at 19:25

    It should find all records with the distance included as field. Unless you specify the limit as additional condition. The case test proofs that AFAIK.

  23. Jure

    September 1, 2015 at 11:42

    Hi Mark,

    it is me again. I checked the TestCase, but I still got the problem. I didn’t get it.
    If I set the distance condition to the option, I get an error – Unknown column ‘distance’ in ‘where clause’

    It looks like I missed something in model?

    Thank you for your help!

  24. Justin McCarty

    August 9, 2016 at 04:50

    Hi am using your Geocoder and have had a lot of fun with it. I was playing with it on localhost computer and it geocodes coords on save and then I used your the custom distance finder which to retrieve the data. When I moved my development to my digital ocean server the customer finder quit working. It returns a sql error: Documentation API
    Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column ‘’ in ‘where clause’.
    I think I’ve read everything in the docs and I can’t figure out what has gone haywire. The code I am running is straight from the docs

    public function searchEvents($lat, $lng)
    		$options = ['lat' => $lat, 'lng' => $lng, 'distance' => 5];
    		$events = $this->Events->find('all', $options);
    		$this->set('events', $events);

    Any help you can give me would be great. Thanks!

  25. Justin McCarty

    August 9, 2016 at 04:53

    Made a mistake. In my testing I had changed the code. I had set the finder to all instead of distance to see if there where other errors.
    the code should be

    public function searchEvents($lat, $lng)
    		$options = ['lat' => $lat, 'lng' => $lng, 'distance' => 5];
    		$events = $this->Events->find('distance', $options);
    		$this->set('events', $events);

    also if you’d like to see the error go here

  26. Justin McCarty

    August 13, 2016 at 04:51

    I got it all worked out. Thanks for the plugin. Well done.

  27. Dan

    May 19, 2017 at 14:44

    Hallo Mark,
    Im new to Cakephp and would like to include Google Maps into my application and display markers from addresses stored in my Database. I came across ur Geocoding Tool and would like to use it. But Im already struggling to get it working.
    After including the tools plugin I followed the first step, but it is causing an error:
    Error: Call to undefined method Cake\Core\App::uses()

    So obviously something is missing.
    Also I opened the GoogleMapsHelper from 2010. But Im not sure where to include the $config file.
    It would be great if u could point me into the right direction.

  28. Mark

    June 8, 2017 at 22:05

    App::uses() is 2.x – which is when this post was written.
    You need to be using use statements now in 3.x.