Zip code library in CodeIgniter

July 7th, 2010

Recently, a client wanted a member search for his website that included a search by zip code. The ‘easy’ solution would be to implement the search as a LIKE statement in SQL, but the solution is inaccurate, and I like the ‘good’ solutions over the ‘easy’ ones. I looked for a CodeIgniter library that would give me the functionality for which I wanted to implement, but no such thing existed. Then, I came across Micah Carrick’s native PHP library which gave the exact functionality I was looking for. I did end up using his library, but not without modification: I’ve ported the native library to a CodeIgniter library. This is where you can get it, but first you should look at the demo.

Demo

The Code

First, you’ll need to create your database. You can use this MySQL script to create the table.

CREATE TABLE `zip_code` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `zip_code` varchar(5) collate utf8_bin NOT NULL,
  `city` varchar(50) collate utf8_bin default NULL,
  `county` varchar(50) collate utf8_bin default NULL,
  `state_name` varchar(50) collate utf8_bin default NULL,
  `state_prefix` varchar(2) collate utf8_bin default NULL,
  `area_code` varchar(3) collate utf8_bin default NULL,
  `time_zone` varchar(50) collate utf8_bin default NULL,
  `lat` float NOT NULL,
  `lon` float NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `zip_code` (`zip_code`)

You will need to populate the table, of course, and you will do so with the MySQL scripts included in the project.

Download the project.

Now, for the meat and potatoes. Here is the library itself. It is GPL Version 3 licensed so keep that in mind if you decide to use it.

  1. <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
  2.  
  3. //constant for convering to kilometers from miles
  4. define('M2KM_FACTOR', 1.609344);
  5.  
  6. // constants for passing $sort to get_zips_in_range()
  7. define('SORT_BY_DISTANCE_ASC', 1);
  8. define('SORT_BY_DISTANCE_DESC', 2);
  9. define('SORT_BY_ZIP_ASC', 3);
  10. define('SORT_BY_ZIP_DESC', 4);
  11.  
  12. class Geozip
  13. {
  14.     var $units;
  15.     var $decimals;
  16.     var $last_error;
  17.     var $CI;
  18.    
  19.     function __construct()
  20.     {
  21.         $this->CI = get_instance();
  22.        
  23.         $this->units = "miles";
  24.         $this->decimals = 2;
  25.     }
  26.    
  27.     function get_zip_details($zip)
  28.     {
  29.         $this->CI->db->select("lat AS lattitude, lon AS longitude, city, county, state_prefix");
  30.         $this->CI->db->select("state_name, area_code, time_zone");
  31.         $this->CI->db->where("zip_code", $zip);
  32.         $results = $this->CI->db->get("zip_code");
  33.        
  34.         if($results->num_rows() < 1)
  35.         {
  36.             $this->set_last_error("Zip not found in database");
  37.             return false;
  38.         }
  39.         else
  40.         {
  41.             return $results->row();
  42.         }
  43.     }
  44.    
  45.     function get_zips_in_range($zip, $range, $sort=1, $include_base=true)
  46.     {
  47.         //get base zip details
  48.         $details = $this->get_zip_point($zip);
  49.        
  50.         if( ! $details)
  51.         {
  52.             return false;
  53.         }
  54.        
  55.         //find max – min lat / long for radius and zero point and query
  56.         //only zips in that range.
  57.         $lat_range = $range/69.172;
  58.         $lon_range = abs($range/(cos($details->lat) * 69.172));
  59.         $min_lat = number_format($details->lat - $lat_range, "4", ".", "");
  60.         $max_lat = number_format($details->lat + $lat_range, "4", ".", "");
  61.         $min_lon = number_format($details->lon - $lon_range, "4", ".", "");
  62.         $max_lon = number_format($details->lon + $lon_range, "4", ".", "");
  63.        
  64.         //build the sql query
  65.         $this->CI->db->select("zip_code, lat, lon");
  66.        
  67.         if( ! $include_base)
  68.         {
  69.             $this->CI->db->where("zip_code <>", $zip);
  70.         }
  71.        
  72.         $this->CI->db->where("lat BETWEEN '$min_lat' AND '$max_lat'");
  73.         $this->CI->db->where("lon BETWEEN '$min_lon' AND '$max_lon'");
  74.        
  75.         $result = $this->CI->db->get("zip_code");
  76.        
  77.         if($result->num_rows() < 1)
  78.         {
  79.             $this->set_last_error("SQL error in get_zips_in_range");
  80.             return false;
  81.         }
  82.         else
  83.         {
  84.             //loop through all 40 some thousand zip codes and determine whether
  85.             //or not it's within the specified range.
  86.            
  87.             foreach($result->result() as $row)
  88.             {
  89.                 $distance = $this->calculate_mileage($details->lat,$row->lat,$details->lon,$row->lon);
  90.                
  91.                 if($this->units == "kilos")
  92.                 {
  93.                     $distance *= M2KM_FACTOR;
  94.                 }
  95.                
  96.                 if($distance <= $range)
  97.                 {
  98.                     $zips[$row->zip_code] = $distance;
  99.                 }
  100.             }
  101.         }
  102.        
  103.         //sort the zips as selected
  104.         switch($sort)
  105.         {
  106.             case SORT_BY_DISTANCE_ASC:
  107.                 asort($zips);
  108.                 break;
  109.            
  110.             case SORT_BY_DISTANCE_DESC:
  111.                 arsort($zips);
  112.                 break;
  113.            
  114.             case SORT_BY_ZIP_ASC:
  115.                 ksort($zips);
  116.                 break;
  117.            
  118.             case SORT_BY_ZIP_DESC:
  119.                 krsort($zips);
  120.                 break;
  121.         }
  122.        
  123.         return $zips;
  124.        
  125.     }
  126.    
  127.     /*
  128.      * Get the distance between 2 zip codes.
  129.      */
  130.     function get_distance($zip1, $zip2)
  131.     {
  132.         //return 0 miles / kilos if its the same zip
  133.         if($zip1 == $zip2)
  134.         {
  135.             return 0;
  136.         }
  137.  
  138.         //get the details from the database and exit if there is an error
  139.         $details1 = $this->get_zip_point($zip1);
  140.         $details2 = $this->get_zip_point($zip2);
  141.        
  142.         if($details1 === false || $details2 === false)
  143.         {
  144.             return false;
  145.         }
  146.        
  147.         //calculate the distance between the 2 zip codes based on
  148.         //the latitude and longitude pulled from the database
  149.         $miles = $this->calculate_mileage($details1->lat,
  150.                                          $details2->lat,
  151.                                          $details1->lon,
  152.                                          $details2->lon);
  153.                                          
  154.                                          
  155.         if($this->units == "kilos")
  156.         {
  157.             return round($miles * M2KM_FACTOR, $this->decimals);
  158.         }
  159.         else
  160.         {
  161.             return round($miles, $this->decimals);
  162.         }    
  163.     }
  164.    
  165.     /*
  166.      * Set the units to describe distance
  167.      * Accepts "miles" or "kilos"
  168.      */
  169.     function set_units($units = "miles")
  170.     {
  171.         if($units != "kilos" || $units != "miles")
  172.         {
  173.             $this->units = "miles";
  174.         }
  175.         else
  176.         {
  177.             $this->units = $units;
  178.         }
  179.     }
  180.    
  181.     function get_last_error()
  182.     {
  183.         return $this->last_error();
  184.     }
  185.    
  186.     /*
  187.      * Pull latitude and longitude from the database
  188.      */
  189.     private function get_zip_point($zip)
  190.     {
  191.         $this->CI->db->select("lat, lon")->where("zip_code", $zip);
  192.         $result = $this->CI->db->get("zip_code");
  193.        
  194.         if($result->num_rows() < 1)
  195.         {
  196.             $this->set_last_error("Zip code not found in db: $zip");
  197.             return false;
  198.         }
  199.         else
  200.         {
  201.             return $result->row();
  202.         }
  203.     }
  204.    
  205.     private function calculate_mileage($lat1, $lat2, $lon1, $lon2)
  206.     {
  207.         //convert lattitude/longitude (degrees) to radians for calculations
  208.         $lat1 = deg2rad($lat1);
  209.         $lon1 = deg2rad($lon1);
  210.         $lat2 = deg2rad($lat2);
  211.         $lon2 = deg2rad($lon2);
  212.          
  213.         //find the deltas
  214.         $delta_lat = $lat2 - $lat1;
  215.         $delta_lon = $lon2 - $lon1;
  216.        
  217.         //find the Great Circle distance
  218.         $temp = pow(sin($delta_lat/2.0),2) + cos($lat1) * cos($lat2) * pow(sin($delta_lon/2.0),2);
  219.         $distance = 3956 * 2 * atan2(sqrt($temp),sqrt(1-$temp));
  220.        
  221.         return $distance;
  222.     }
  223.    
  224.     private function set_last_error($error)
  225.     {
  226.         $this->last_error = $error;
  227.     }
  228. }

How to use

The library supports 3 main features right now: get zip details, get distance between 2 zip codes, and get a list of zip codes in a given radius of another zip code. The library supports miles and kilometers as distance units. I’ll demonstrate how to use each of these functions, and how to change the units.

Copy the code above into a file named geozip.php and place it in the application/libraries folder in your CodeIgniter installation. Then, in whatever controller you want to use it in, load it like you would any other library.

$this->load->library("geozip");

Getting and displaying the zip’s details are simple. In the controller, you would do

$zip_details = $this->geozip->get_zip_details($this->input->post("zip"));
$data['zip_details'] = $zip_details;

and then in the view, you could access the following variables:

echo $zip_details->lattitude;
echo $zip_details->longitude;
echo $zip_details->city;
echo $zip_details->county;
echo $zip_details->state_prefix;
echo $zip_details->state_name;
echo $zip_details->area_code;
echo $zip_details->time_zone;

Getting the distance between 2 zips is equally as easy.

$distance = $this->geozip->get_distance("90210", "60601");

Doing a radial search is only slightly more complicated, but only because you have some options when you call the function. The function accepts 4 parameters: the zip code, the range to cover, sort order, and whether or not to include the original zip in the result set. The default sort order is sorted by distance ascending, and the default ‘include base’ is true. You can change the default sort order using the following defines, and you can change the ‘include base’ by setting it to boolean false.

// constants for passing $sort to get_zips_in_range()
define('SORT_BY_DISTANCE_ASC', 1);
define('SORT_BY_DISTANCE_DESC', 2);
define('SORT_BY_ZIP_ASC', 3);
define('SORT_BY_ZIP_DESC', 4);

Here is how you could find all the zips in a 20 mile radius of Beverly Hills sorted by zip ascending, but not including Beverly Hills.

$zips = $this->geozip->get_zips_in_range("90210", 20, SORT_BY_ZIP_ASC, false);
$data['zips'] = $zips;

$zips will be an associative array with the zip code as the key and the distance from the origin zip as the value. You can process this in your controller or view as you see fit.

foreach($zips as $zip => $distance)
{
    echo "$zip is $distance miles away from the origin.";
	//or other processing you wish to do with these zips.
}

I did say that the library supports miles and kilometers. To change between them, simply do

$this->geozip->set_units("kilos"); //only accepts "miles" or "kilos"

And any function that returns a distance will return whichever unit you’ve specified.

Things to remember

The biggest problem with this approach is the database. As stated in the original blog by Micah, the database was derived from a variety of sources and may be out of date. Depending on how critical this feature is to your application, the current database may or may not work. If you want an up to date database, you’ll probably have to purchase and import it yourself.

You can download an example application that is ready to deploy, but it is likely to become outdated as time passes and I make feature and bug changes. You can always get the latest version in the BitBucket repo.

If you have any questions or comments, post them here and I’ll be sure to answer them! You can also contact me if your issues should remain private. Happy coding!

2 Responses to “Zip code library in CodeIgniter”

  1. John says:

    Nice article. Now just make your demo work :) When I get home I’ll have to actually play with the class to see how well it works.

  2. admin says:

    Thanks for the heads up. It’s working now :)

Leave a Reply