Zip/Postal Code Lookup Class

Mar 03

If you’ve ever needed the ability to lookup geographical information for a particular zip or postal code in PHP, look no further! This is your all-in-one geo data lookup class. This class will query a database and get the city, province (or state), country, area code, time zone, and coordinates. It can also calculate distances between two different zip/postal codes.

What sets this class apart from others is its simplicity. At only 172 lines, most of the processing is done on the server end. This class can also learn! If you query for a postal/zip code that isn’t found in the database the server will go out and fetch information from other sources, parse it, save it, and return it to you. If all those sources fail it will try to get you details of the next closest zip/postal code and when that fails you’ll receive a 404 status error.

Details of zip/postal codes are stored on my server and is available to anyone at anytime. The data has been gathered from sources such as Canada Post and the USPS. I currently have a database of 900,000 codes and growing daily the more it’s used.

All coordinates are pulled from Google’s MAP API because of the accuracy provided by Google (the coordinates provided by Canada Post and the USPS are of by about 1 or 2 kms in rural areas)

Enjoy and don’t forget to let me know what you think!

Demo

Try it out here: http://www.eyesis.ca/demos/eyegeodata/

Limits

I’ve applied a lookup limit of 1000 queries a day. I think this is more than reasonable for the average person. If you require more just drop me a line and we’ll work something out.

By default, SimpleXML is used to fetch data from the feed. Alternatively, you can configure this class to use PHP’s serialize function to retrieve data. PHP 5 only.

Download

The latest version was released March 3, 2011.

Download eyegeodata-1.0.zip

Looking Up Information

To fetch information on a particular zip/postal code, use the “query” function. Results are returned in an array. Shown below is a Postal code in Toronto and a Zip code for Beverly Hills.

include 'class.geodata.inc.php';

$lookup = new EyeGeoData();

// Lookup postal code M5X 1J2
print_r($lookup->query('M5X 1J2'));
/*Array
(
    [PostalCode] => M5X1J2
    [City] => TORONTO
    [Province] => ON
    [Country] => CA
    [AreaCode] => 416
    [TimeZone] => 5
    [Coordinates] => Array
        (
            [Latitude] => 43.648306
            [Longitude] => -79.38224
        )

)*/

// Lookup zip code 90210
print_r($lookup->query('90210'));
/*Array
(
    [PostalCode] => 90210
    [City] => BEVERLY HILLS
    [Province] => CA
    [Country] => US
    [AreaCode] => 310
    [TimeZone] => 8
    [Coordinates] => Array
        (
            [Latitude] => 34.103131
            [Longitude] => -118.416253
        )

)*/

Calculating Distance Between Zip/Postal Codes

Need to calculate the distance between two different postal or zip codes? Easy! Check it out. The results can be returned in miles or kms and rounded to any number of decimal places.


include 'class.geodata.inc.php';

// Compare the distance between two different zip/postal codes
$x = new EyeGeoData();
$x->query('M5X 1J2');

$y = new EyeGeoData();
$y->query('90210');

echo $x->calcDistance($y, EyeGeoData::KMS, 2) . ' kms'; // prints "6923.05 kms"

Checking Status Codes

Here is a list of possible status codes. I used HTTP status codes for simplicity.

  • 200    All is good
  • 400    Bad request, not properly formatted zip/postal code
  • 403    Forbidden, exceeded daily lookup limit
  • 404    Could not find zip/postal code
require 'class.geodata.inc.php';

$geo = new EyeGeoData();
$result = $geo->query($postal);
$status = $geo->getStatusCode();

// Status code of 200 means success
if ($status == 200)
print_r($result);
else
echo 'Error: ' . $status;

The EyeGeoData Class

Here is the EyeGeoData class, you can download it below:

/**
* EyeGeoData
* Retrieve geographical information of a postal/zip/post code
*
* LICENSE: This source file is subject to the BSD license
* that is available through the world-wide-web at the following URI:
* http://www.eyesis.ca/license.txt.  If you did not receive a copy of
* the BSD License and are unable to obtain it through the web, please
* send a note to mike@eyesis.ca so I can send you a copy immediately.
*
* @author     Micheal Frank <mike@eyesis.ca>
* @copyright  2011 Eyesis
* @license    http://www.eyesis.ca/license.txt  BSD License
* @version    v1.0.1 3/3/2011 2:58:39 PM
* @link       http://www.eyesis.ca/projects/geodata.html
*/

class EyeGeoData
{
const FEED = 'http://api.eyesis.ca/geo.xml';

const KMS = 1;
const MIS = 2;

private $key = false;
private $status_code, $lookups_left;
public $result = false;
public $out = 'xml';

/**
* Set a key for premium accounts
*
* @param string $key Key provided by Eyesis for premium accounts
*/
public function setKey($key)
{

$this->key = $key;

}

/**
* Get postal/zip code information
*
* @param string $code The postal/zip code you want to lookup
* @return mixed The result of the query, false on error
*/
public function query($code)
{

// Build the query
$query = '?code=' . urlencode($code);
if ($this->key)
$query .= '&key=' . urlencode($this->key);

$info = false;

if ($this->out == 'xml')
{
// Use XML feed and SimpleXML

if ($feed = new SimpleXMLElement(self::FEED . $query, NULL, true))
{

$this->status_code        = (integer) $feed->Request->StatusCode;
$this->lookups_left        = (integer) $feed->Client->LookupsLeft;

if ($this->status_code == 200)
{
$info = array (
'PostalCode'     => (string) $feed->Details->PostalCode,
'City'                 => (string) $feed->Details->City,
'Province'         => (string) $feed->Details->Province,
'Country'         => (string) $feed->Details->Country,
'AreaCode'         => (integer) $feed->Details->AreaCode,
'TimeZone'         => (integer) $feed->Details->TimeZone,
'Coordinates' => array (
'Latitude'         => (float) $feed->Details->Coordinates->Latitude,
'Longitude'     => (float) $feed->Details->Coordinates->Longitude
)
);
}

}

} else {
// Use Serialized feed

if ($feed = file_get_contents(self::FEED . $query . '&out=php'))
{

$feed = unserialize($feed);

$this->status_code        = (integer) $feed['PostalCode']['Request']['StatusCode'];
$this->lookups_left        = (integer) $feed['PostalCode']['Client']['LookupsLeft'];

if ($this->status_code == 200)
$info = $feed['PostalCode']['Details'];

}

}

$this->result = $info;

return $this->result;

}

/**
* Get the status code of the previous result
*
* @return mixed
*/
public function getStatusCode()
{

return ($this->status_code) ? $this->status_code : false;

}

/**
* Get the amount of lookups left
*
* @return mixed
*/
public function getLookupsLeft()
{

return ($this->lookups_left) ? $this->lookups_left : false;

}

/**
* Calculates the distance between this result and another GeoData object's result
*
* @param GeoData $dest The object to compare to
* @param integer $units The distance units
* @return float
*/
public function calcDistance(EyeGeoData $dest, $units = self::KMS, $round = 3)
{

if (!$dest->result or !$this->result)
trigger_error('Result empty; query either failed or has not been run', E_USER_ERROR);

$dist = rad2deg(acos(sin(deg2rad($this->result['Coordinates']['Latitude'])) * sin(deg2rad($dest->result['Coordinates']['Latitude'])) +
cos(deg2rad($this->result['Coordinates']['Latitude'])) * cos(deg2rad($dest->result['Coordinates']['Latitude'])) *
cos(deg2rad($this->result['Coordinates']['Longitude'] - $dest->result['Coordinates']['Longitude'])))) * 60 * 1.1515;

// Convert to kms
if ($units == self::KMS)
$dist *= 1.609344;

$dist = round($dist, $round); // Round off

return (float) $dist;

}

/**
* Convert this object to a string
*
* @return string
*/
public function __toString()
{

if ($this->result)
return print_r($this->result, true);
else
return "I've got nothing to say";

}
}