Question

I have a working PHP function that takes in latitude/longitude coordinates and a distance in miles, then tests row by row in a MySQL table of zip codes/lat-lon pairs (40,000+ rows) whether each zip code/lat-lon row falls within the specified distance or not.

I know that this method is terribly inefficient and I would like to do all of the trigonometry and distance calculation in the SQL query rather than with PHP. Can anyone help me out here? I feel like I've tried everything but I just can't get the SQL query correct. Any help would be greatly appreciated!

Existing PHP function:

function getZips($lat, $lon, $radius)
{   

//MySQL bit
$zipQuery = mysql_query("SELECT * FROM zip_codes");
while ($row = mysql_fetch_assoc($zipQuery))
{

    $lat2 = $row['latitude'];
    $lon2 = $row['longitude'];

    $distance = (3958*3.1415926*sqrt(($lat2-$lat)*($lat2-$lat) + cos($lat2/57.29578)*cos($lat/57.29578)*($lon2-$lon)*($lon2-$lon))/180);

    if ($distance <= $radius)
    {
        $zip = $row['zip'];
        echo "Zip in range: <b>" .$zip . "</b><br>";
    }

}

}
Was it helpful?

Solution

SELECT zipcode, ( 3959 * acos( cos( radians( {$coords['latitude']} ) ) * cos( radians( latitude ) ) * cos( radians( longitude ) - radians( {$coords['longitude']} ) ) + sin( radians( {$coords['latitude']} ) ) * sin( radians( latitude ) ) ) ) AS distance FROM logic_zipcodes HAVING distance <= {$radius} ORDER BY distance

can't remember where I got this originaly, but I have been using it for a long time. Here is my function:

function zipsearch($start, $rad) {
// ITITIAL POINT
$result = mysql_query("SELECT latitude,longitude FROM logic_zipcodes WHERE zipcode='" . $start . "'");
while($row = mysql_fetch_array($result)) {
    $coords = array('latitude' => $row['latitude'], 'longitude' => $row['longitude']);
}
//RADIUS
$radius = $rad;
// SQL FOR MILES
$sql = "SELECT zipcode, ( 3959 * acos( cos( radians( {$coords['latitude']} ) ) * cos( radians( latitude ) ) * cos( radians( longitude ) - radians( {$coords['longitude']} ) ) + sin( radians( {$coords['latitude']} ) ) * sin( radians( latitude ) ) ) ) AS distance FROM logic_zipcodes HAVING distance <= {$radius} ORDER BY distance";

// OUTPUT THE ZIPCODES AND DISTANCES
$query = mysql_query($sql);
$zips = array();
while($row = mysql_fetch_assoc($query)) {
    $zips[] = $row['zipcode'];
}
return $zips;
}
 function getZips($zip, $rad) {
$zipcode = qt($zip);
$andstatement = implode("','", zipsearch($zipcode, $rad));
return "AND zipcode in('" . $andstatement . "')";
 }

OTHER TIPS

here's a list of all states/cities linked up with lat and long. https://stackoverflow.com/questions/14272153/where-can-i-find-a-table-of-usas-zipcodes-lat-and-lon/14272155#14272155

As for your trig, Do it in Mysql and everything else in PHP:

 <?php
    /**
     *  @package    zipcode
     */


    class ZipCode
    {
        private $zip_code_id;
        private $zip_code;
        private $lat;
        private $lon;
        private $city;
        private $county;
        private $area_code;
        private $time_zone;
        private $state_prefix;
        private $state_name;

        public $mysql_table = 'zip_code';
        public $mysql_conn = false;
        private $mysql_row;

        private $print_name;
        private $location_type;

        const UNIT_MILES = 1;
        const UNIT_KILOMETERS = 2;
        const MILES_TO_KILOMETERS = 1.609344;

        const LOCATION_ZIP = 1;
        const LOCATION_CITY_STATE = 2;

        /**
         *  Constructor
         *
         *  Instantiate a new ZipCode object by passing in a location. The location
         *  can be specified by a string containing a 5-digit zip code, city and 
         *  state, or latitude and longitude.
         *
         *  @param  string
         *  @return ZipCode
         */
        public function __construct($location) 
        {
            if (is_array($location)) {
                $this->setPropertiesFromArray($location);
                $this->print_name = $this->zip_code;
                $this->location_type = $this::LOCATION_ZIP;
            } else {
                $this->location_type = $this->locationType($location);

                switch ($this->location_type) {

                    case ZipCode::LOCATION_ZIP:
                        $this->zip_code = $this->sanitizeZip($location);
                        $this->print_name = $this->zip_code;
                        break;

                    case ZipCode::LOCATION_CITY_STATE:
                        $a = $this->parseCityState($location);
                        $this->city = $a[0];
                        $this->state_prefix = $a[1];
                        $this->print_name = $this->city;
                        break;

                    default:
                        throw new Exception('Invalid location type for '.__CLASS__);
                }
            }
        }

        public function __toString()
        {
            return $this->print_name;
        }

        /**
        *   Calculate Distance using SQL
        *
        *   Calculates the distance, in miles, to a specified location using MySQL
        *   math functions within the query.
        *
        *   @access private
        *   @param  string
        *   @return float
        */
        private function calcDistanceSql($location)
        {
            $sql = 'SELECT 3956 * 2 * ATAN2(SQRT(POW(SIN((RADIANS(t2.lat) - '
                  .'RADIANS(t1.lat)) / 2), 2) + COS(RADIANS(t1.lat)) * '
                  .'COS(RADIANS(t2.lat)) * POW(SIN((RADIANS(t2.lon) - '
                  .'RADIANS(t1.lon)) / 2), 2)), '
                  .'SQRT(1 - POW(SIN((RADIANS(t2.lat) - RADIANS(t1.lat)) / 2), 2) + '
                  .'COS(RADIANS(t1.lat)) * COS(RADIANS(t2.lat)) * '
                  .'POW(SIN((RADIANS(t2.lon) - RADIANS(t1.lon)) / 2), 2))) '
                  .'AS "miles" '
                  ."FROM {$this->mysql_table} t1 INNER JOIN {$this->mysql_table} t2 ";


            switch ($this->location_type) {

                case ZipCode::LOCATION_ZIP:
                    // note: zip code is sanitized in the constructor
                    $sql .= "WHERE t1.zip_code = '{$this->zip_code}' ";
                    break;

                case ZipCode::LOCATION_CITY_STATE:
                    $city = @mysql_real_escape_string($this->city);
                    $state = @mysql_real_escape_string($this->state_prefix);
                    $sql .= "WHERE (t1.city = '$city' AND t1.state_prefix = '$state') AND t2.zip_code = '$zip_to'";
                    break;

                default:
                    throw new Exception('Invalid location type for '.__CLASS__);
            }

            switch (ZipCode::locationType($location))
            {
                case ZipCode::LOCATION_ZIP:
                    $zip_to = $this->sanitizeZip($location);
                    $sql .= "AND t2.zip_code = '$zip_to'";
                    break;
                case ZipCode::LOCATION_CITY_STATE:
                    $a = $this->parseCityState($location);
                    $city = @mysql_real_escape_string($a[0]);
                    $state = @mysql_real_escape_string($a[1]);
                    $sql .= "AND (t2.city = '$city' AND t2.state_prefix = '$state')";
                    break;
            }

            $r = @mysql_query($sql);

            if (!$r) {
                throw new Exception(mysql_error());
            }

            if (mysql_num_rows($r) == 0) {
                throw new Exception("Record does not exist calculating distance between $zip_from and $zip_to");
            }

            $miles = mysql_result($r, 0);
            mysql_free_result($r);

            return $miles;
        }

        public function getAreaCode()
        {
            if (empty($this->zip_code_id)) $this->setPropertiesFromDb();
            return $this->city;
        }

        public function getCity()
        {
            if (empty($this->zip_code_id)) $this->setPropertiesFromDb();
            return $this->city;
        }

        public function getCounty()
        {
            if (empty($this->zip_code_id)) $this->setPropertiesFromDb();
            return $this->county;
        }

        public function getStateName()
        {
            if (empty($this->zip_code_id)) $this->setPropertiesFromDb();
            return $this->state_name;
        }

        public function getStatePrefix()
        {
            if (empty($this->zip_code_id)) $this->setPropertiesFromDb();
            return $this->state_prefix;
        }

        public function getDbRow()
        {
            if (empty($this->zip_code_id)) $this->setPropertiesFromDb();
            return $this->mysql_row;
        }

        /**
        *   Get Distance To Zip
        *
        *   Gets the distance to another zip code. The distance can be obtained in
        *   either miles or kilometers.
        *
        *   @param  string
        *   @param  integer
        *   @param  integer
        *   @return float
        */
        public function getDistanceTo($zip, $units=ZipCode::UNIT_MILES)
        {
            $miles = $this->calcDistanceSql($zip);

            if ($units == ZipCode::UNIT_KILOMETERS) {
                return $miles * ZipCode::MILES_TO_KILOMETERS;
            } else {
                return $miles;
            }
        }

        public function getZipsInRange($range_from, $range_to, $units=1)
        {
            if (empty($this->zip_code_id)) $this->setPropertiesFromDb();

            $sql = "SELECT 3956 * 2 * ATAN2(SQRT(POW(SIN((RADIANS({$this->lat}) - "
                  .'RADIANS(z.lat)) / 2), 2) + COS(RADIANS(z.lat)) * '
                  ."COS(RADIANS({$this->lat})) * POW(SIN((RADIANS({$this->lon}) - "
                  ."RADIANS(z.lon)) / 2), 2)), SQRT(1 - POW(SIN((RADIANS({$this->lat}) - "
                  ."RADIANS(z.lat)) / 2), 2) + COS(RADIANS(z.lat)) * "
                  ."COS(RADIANS({$this->lat})) * POW(SIN((RADIANS({$this->lon}) - "
                  ."RADIANS(z.lon)) / 2), 2))) AS \"miles\", z.* FROM {$this->mysql_table} z "
                  ."WHERE zip_code <> '{$this->zip_code}' " 
                  ."AND lat BETWEEN ROUND({$this->lat} - (25 / 69.172), 4) "
                  ."AND ROUND({$this->lat} + (25 / 69.172), 4) "
                  ."AND lon BETWEEN ROUND({$this->lon} - ABS(25 / COS({$this->lat}) * 69.172)) "
                  ."AND ROUND({$this->lon} + ABS(25 / COS({$this->lat}) * 69.172)) "
                  ."AND 3956 * 2 * ATAN2(SQRT(POW(SIN((RADIANS({$this->lat}) - "
                  ."RADIANS(z.lat)) / 2), 2) + COS(RADIANS(z.lat)) * "
                  ."COS(RADIANS({$this->lat})) * POW(SIN((RADIANS({$this->lon}) - "
                  ."RADIANS(z.lon)) / 2), 2)), SQRT(1 - POW(SIN((RADIANS({$this->lat}) - "
                  ."RADIANS(z.lat)) / 2), 2) + COS(RADIANS(z.lat)) * "
                  ."COS(RADIANS({$this->lat})) * POW(SIN((RADIANS({$this->lon}) - "
                  ."RADIANS(z.lon)) / 2), 2))) <= $range_to "
                  ."AND 3956 * 2 * ATAN2(SQRT(POW(SIN((RADIANS({$this->lat}) - "
                  ."RADIANS(z.lat)) / 2), 2) + COS(RADIANS(z.lat)) * "
                  ."COS(RADIANS({$this->lat})) * POW(SIN((RADIANS({$this->lon}) - "
                  ."RADIANS(z.lon)) / 2), 2)), SQRT(1 - POW(SIN((RADIANS({$this->lat}) - "
                  ."RADIANS(z.lat)) / 2), 2) + COS(RADIANS(z.lat)) * "
                  ."COS(RADIANS({$this->lat})) * POW(SIN((RADIANS({$this->lon}) - "
                  ."RADIANS(z.lon)) / 2), 2))) >= $range_from "
                  ."ORDER BY 1 ASC";

            $r = mysql_query($sql);
            if (!$r) {
                throw new Exception(mysql_error());
            }
            $a = array();
            while ($row = mysql_fetch_array($r, MYSQL_ASSOC))
            {
                // TODO: load ZipCode from array
                $a[$row['miles']] = new ZipCode($row);
            }

            return $a;
        }

        private function hasDbConnection()
        {
            if ($this->mysql_conn) {
                return mysql_ping($this->mysql_conn);
            } else {
                return mysql_ping();
            }
        }



        private function locationType($location)
        {
            if (ZipCode::isValidZip($location)) {
                return ZipCode::LOCATION_ZIP;
            } elseif (ZipCode::isValidCityState($location)) {
                return ZipCode::LOCATION_CITY_STATE;
            } else {
                return false;
            }
        }

        static function isValidZip($zip)
        { 
            return preg_match('/^[0-9]{5}/', $zip);
        }

        static function isValidCityState($location)
        { 
            $words = split(',', $location);

            if (empty($words) || count($words) != 2 || strlen(trim($words[1])) != 2) {
                return false;
            }

            if (!is_numeric($words[0]) && !is_numeric($words[1]))  {
                return true;
            }

            return false;
        }

        static function parseCityState($location)
        {
            $words = split(',', $location);

            if (empty($words) || count($words) != 2 || strlen(trim($words[1])) != 2) {
                throw new Exception("Failed to parse city and state from string.");
            }

            $city = trim($words[0]);
            $state = trim($words[1]);

            return array($city, $state);
        }

        // @access protected
        private function sanitizeZip($zip)
        {
            return preg_replace("/[^0-9]/", '', $zip);
        }

        private function setPropertiesFromArray($a)
        {    
            if (!is_array($a)) {
                throw new Exception("Argument is not an array");
            }

            foreach ($a as $key => $value)
            {
                $this->$key = $value;
            }

            $this->mysql_row = $a;
        }

        private function setPropertiesFromDb()
        {
            switch ($this->location_type) {

                case ZipCode::LOCATION_ZIP:
                    $sql = "SELECT * FROM {$this->mysql_table} t "
                          ."WHERE zip_code = '{$this->zip_code}' LIMIT 1";
                    break;

                case ZipCode::LOCATION_CITY_STATE:
                    $sql = "SELECT * FROM {$this->mysql_table} t "
                          ."WHERE city = '{$this->city}' "
                          ."AND state_prefix = '{$this->state_prefix}' LIMIT 1";
                    break;
            }

            $r = mysql_query($sql);
            $row = mysql_fetch_array($r, MYSQL_ASSOC);
            mysql_free_result($r);

            if (!$row)
            {
                throw new Exception("{$this->print_name} was not found in the database.");
            }

            $this->setPropertiesFromArray($row);
        }
    }

    ?>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top