Cum pot implementa o căutare bazată pe locație (cod poștal) în WordPress?
Lucrez la un site de tip director pentru afaceri locale care va folosi custom post types pentru înregistrările companiilor. Unul dintre câmpuri va fi "Cod Poștal". Cum pot configura o căutare bazată pe locație?
Mi-ar plăcea ca vizitatorii să poată introduce codul lor poștal și să aleagă o categorie pentru a afișa toate afacerile într-o anumită rază sau toate afacerile ordonate după distanță. Am văzut câteva plugin-uri care pretind că fac acest lucru, dar nu sunt compatibile cu WordPress 3.0. Aveți sugestii?

Eu aș modifica răspunsul de la gabrielk și postul de blog legat prin utilizarea indexurilor de bază de date și minimizarea numărului de calcule efective de distanță.
Dacă cunoști coordonatele utilizatorului și distanța maximă (de exemplu 10km), poți desena un dreptunghi de delimitare de 20km pe 20km cu locația curentă în centru. Obține aceste coordonate de delimitare și interoghează doar magazinele dintre aceste latitudini și longitudini. Nu utiliza încă funcții trigonometrice în interogarea bazei de date, deoarece acest lucru va împiedica utilizarea indexurilor. (Deci, s-ar putea să obții un magazin care este la 12km distanță de tine dacă se află în colțul nord-estic al dreptunghiului de delimitare, dar îl vom elimina în pasul următor.)
Calculează distanța (în linie dreaptă sau cu direcții reale de condus, după preferință) doar pentru puținele magazine returnate. Acest lucru va îmbunătăți drastic timpul de procesare dacă ai un număr mare de magazine.
Pentru căutarea înrudită ("dă-mi cele zece magazine cele mai apropiate") poți face o căutare similară, dar cu o estimare inițială a distanței (deci începi cu o zonă de 10km pe 10km, iar dacă nu ai suficiente magazine, o extinzi la 20km pe 20km și așa mai departe). Pentru această estimare inițială a distanței, calculezi o dată numărul de magazine din întreaga zonă și îl folosești. Sau poți înregistra numărul de interogări necesare și să te adaptezi în timp.
Am adăugat un exemplu complet de cod la întrebarea înrudită a lui Mike, iar aici este o extensie care îți oferă cele mai apropiate X locații (rapid și testat minimal):
class Monkeyman_Geo_ClosestX extends Monkeyman_Geo
{
public static $closestXStartDistanceKm = 10;
public static $closestXMaxDistanceKm = 1000; // Nu căuta dincolo de aceasta
public function addAdminPages()
{
parent::addAdminPages();
add_management_page( 'Test cele mai apropiate locații', 'Test cele mai apropiate locații', 'edit_posts', __FILE__ . 'closesttest', array(&$this, 'doClosestTestPage'));
}
public function doClosestTestPage()
{
if (!array_key_exists('search', $_REQUEST)) {
$default_lat = ini_get('date.default_latitude');
$default_lon = ini_get('date.default_longitude');
echo <<<EOF
<form action="" method="post">
<p>Număr de postări: <input size="5" name="post_count" value="10"/></p>
<p>Latitudinea centrului: <input size="10" name="center_lat" value="{$default_lat}"/>
<br/>Longitudinea centrului: <input size="10" name="center_lon" value="{$default_lon}"/></p>
<p><input type="submit" name="search" value="Caută!"/></p>
</form>
EOF;
return;
}
$post_count = intval($_REQUEST['post_count']);
$center_lon = floatval($_REQUEST['center_lon']);
$center_lat = floatval($_REQUEST['center_lat']);
var_dump(self::getClosestXPosts($center_lon, $center_lat, $post_count));
}
/**
* Obține cele mai apropiate X postări de o locație dată
*
* Acest lucru poate returna mai multe decât X rezultate și niciodată mai mult decât
* self::$closestXMaxDistanceKm distanță (pentru a preveni căutarea fără sfârșit)
* Rezultatele sunt sortate după distanță
*
* Algoritmul începe cu toate locațiile care nu sunt mai departe decât
* self::$closestXStartDistanceKm, apoi extinde această zonă
* (prin dublarea distanței) până când sunt găsite suficiente potriviri.
*
* Numărul de calcule costisitoare ar trebui minimizat.
*/
public static function getClosestXPosts($center_lon, $center_lat, $post_count)
{
$search_distance = self::$closestXStartDistanceKm;
$close_posts = array();
while (count($close_posts) < $post_count && $search_distance < self::$closestXMaxDistanceKm) {
list($north_lat, $east_lon, $south_lat, $west_lon) = self::getBoundingBox($center_lat, $center_lon, $search_distance);
$geo_posts = self::getPostsInBoundingBox($north_lat, $east_lon, $south_lat, $west_lon);
foreach ($geo_posts as $geo_post) {
if (array_key_exists($geo_post->post_id, $close_posts)) {
continue;
}
$post_lat = floatval($geo_post->lat);
$post_lon = floatval($geo_post->lon);
$post_distance = self::calculateDistanceKm($center_lat, $center_lon, $post_lat, $post_lon);
if ($post_distance < $search_distance) {
// Include doar cele care sunt în raza cercului, nu în dreptunghiul de delimitare, altfel am putea rata unele mai apropiate în pasul următor
$close_posts[$geo_post->post_id] = $post_distance;
}
}
$search_distance *= 2;
}
asort($close_posts);
return $close_posts;
}
}
$monkeyman_Geo_ClosestX_instace = new Monkeyman_Geo_ClosestX();

În primul rând ai nevoie de un tabel care arată cam așa:
zip_code lat lon
10001 40.77 73.98
...completat pentru fiecare cod poștal. Poți extinde acest tabel prin adăugarea câmpurilor pentru oraș și stat dacă dorești să faci căutări și după aceste criterii.
Apoi fiecărui magazin i se poate atribui un cod poștal, iar când trebuie să calculezi distanța poți face join între tabelul cu latitudine/longitudine și datele magazinului.
Ulterior vei interoga acest tabel pentru a obține latitudinea și longitudinea pentru codurile poștale ale magazinului și ale utilizatorului. După ce le obții, poți popula array-ul și să-l transmiți unei funcții "get distance":
$user_location = array(
'latitude' => 42.75,
'longitude' => 73.80,
);
$output = array();
$results = $wpdb->get_results("SELECT id, zip_code, lat, lon FROM store_table");
foreach ( $results as $store ) {
$store_location = array(
'zip_code' => $store->zip_code, // 10001
'latitude' => $store->lat, // 40.77
'longitude' => $store->lon, // 73.98
);
$distance = get_distance($store_location, $user_location, 'miles');
$output[$distance][$store->id] = $store_location;
}
ksort($output);
foreach ($output as $distance => $store) {
foreach ( $store as $id => $location ) {
echo 'Magazinul ' . $id . ' se află la ' . $distance . ' distanță';
}
}
function get_distance($store_location, $user_location, $units = 'miles') {
if ( $store_location['longitude'] == $user_location['longitude'] &&
$store_location['latitude'] == $user_location['latitude']){
return 0;
$theta = ($store_location['longitude'] - $user_location['longitude']);
$distance = sin(deg2rad($store_location['latitude'])) * sin(deg2rad($user_location['latitude'])) + cos(deg2rad($store_location['latitude'])) * cos(deg2rad($user_location['latitude'])) * cos(deg2rad($theta));
$distance = acos($distance);
$distance = rad2deg($distance);
$distance = $distance * 60 * 1.1515;
if ( 'kilometers' == $units ) {
$distance = $distance * 1.609344;
}
return round($distance);
}
Acesta este doar un concept demonstrativ, nu un cod pe care l-aș recomanda pentru implementare. Dacă ai, de exemplu, 10.000 de magazine, ar fi o operațiune destul de costisitoare să interoghezi toate datele și să le sortezi la fiecare solicitare.

Pot fi rezultatele stocate în cache? De asemenea, ar fi mai ușor să interogăm una dintre bazele de date de coduri poștale disponibile comercial (sau gratuite, dacă există)?

@matt - Ce înțelegi prin interogarea uneia dintre opțiunile comerciale sau gratuite disponibile? Stocarea în cache ar trebui să fie întotdeauna posibilă, vezi http://codex.wordpress.org/Transients_API

@hakre: Nu mai contează, cred că vorbesc peste capacitatea mea acum. Mă refeream la utilizarea unei baze de date de coduri poștale (USPS, Google Maps...) pentru a obține distanțele, dar nu mi-am dat seama că probabil ele nu stochează distanțele, ci doar codul poștal și coordonatele, iar mie mi-ar rămâne să le calculez.

Documentația MySQL include și informații despre extensiile spațiale. În mod ciudat, funcția standard distance() nu este disponibilă, dar verificați această pagină: http://dev.mysql.com/tech-resources/articles/4.1/gis-with-mysql.html pentru detalii despre cum să "convertiți cele două valori POINT într-un LINESTRING și apoi să calculați lungimea acestuia."
Rețineți că fiecare furnizor este probabil să ofere latitudini și longitudini diferite reprezentând "centroidul" unui cod poștal. De asemenea, merită să știți că nu există fișiere reale definite pentru "granițele" codurilor poștale. Fiecare furnizor va avea propriul set de granițe care se potrivesc aproximativ cu listele specifice de adrese care alcătuiesc un cod poștal USPS. (De exemplu, în unele "granițe" ar trebui să includeți ambele părți ale străzii, în altele, doar una.) Zonele de Tabulație a Codurilor Poștale (ZCTAs), utilizate pe scară largă de furnizori, "nu descriu cu precizie zonele de livrare a codurilor poștale și nu includ toate codurile poștale utilizate pentru livrarea poștei" http://www.census.gov/geo/www/cob/zt_metadata.html
Multe afaceri din centrul orașelor vor avea propriul cod poștal. Veți dori un set de date cât mai complet, așa că asigurați-vă că găsiți o listă de coduri poștale care include atât coduri poștale "punct" (de obicei afaceri), cât și coduri poștale "graniță".
Am experiență în lucrul cu datele despre codurile poștale de la http://www.semaphorecorp.com/. Chiar și acelea nu au fost 100% precise. De exemplu, când campusul meu a primit o nouă adresă poștală și un nou cod poștal, codul a fost plasat greșit. Cu toate acestea, a fost singura sursă de date pe care am găsit-o care avea chiar și noul cod poștal, atât de curând după ce a fost creat.
Aveam o rețetă în cartea mea pentru exact cum să vă îndepliniți cerința... în Drupal. Se baza pe modulul Google Maps Tools ( http://drupal.org/project/gmaps, nu trebuie confundat cu http://drupal.org/project/gmap, de asemenea un modul valoros.) S-ar putea să găsiți niște coduri de exemplu utile în acele module, desigur, ele nu vor funcționa direct în WordPress.
