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.
Geocoding
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:
Distance
$result = $this->Geocode->distance($pointOne, $pointTwo); // array('lat'=>..., 'lng'=>...);
returns the distance between those two points.
Convert
$result = $this->Geocode->convert($value, $fromUnit, $toUnit);
helps to convert miles to km etc.
Blur
$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 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 didn’t get to this part yet.
Searching/Filtering
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->Behaviors->attach('Tools.Geocoder'); $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.
Validation
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, ); $this->Address->set($data); $res = $this->Address->validates(); // will return false in this case
Todos
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.
