Add ghost text autocomplete location search to homepage hero

Replaces community dropdown with text input supporting city/ZIP search:
- Ghost text autocomplete shows inline suggestion as user types
- Tab to accept, auto-fill on blur, Enter uses partial match
- Geolocation button for "Use My Location" searches
- AJAX endpoint returns MN/IA cities and zipcodes with 1-hour cache
- MLS query now supports lat/lng/radius for distance-based filtering
- Updated Census Bureau 2023 Gazetteer data (32,329 US cities)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Hanson.xyz Dev
2025-12-29 00:50:45 -06:00
parent 183e1b92c9
commit 5ca2e29c72
13 changed files with 33288 additions and 29965 deletions
+204 -16
View File
@@ -50,6 +50,85 @@ class MLS_Query {
return "(street_number IS NULL OR (street_number != 'TBD' AND street_number NOT LIKE 'TBD %'))";
}
/**
* Get coordinates for a zip code from geo table
*
* @param string $zipcode The zip code to look up
* @return array|null Array with 'latitude' and 'longitude' or null if not found
*/
private function get_zipcode_coordinates($zipcode) {
global $wpdb;
$table = $this->db->geo_zipcodes_table();
$result = $wpdb->get_row($wpdb->prepare(
"SELECT latitude, longitude FROM {$table} WHERE zipcode = %s",
$zipcode
));
if ($result) {
return array(
'latitude' => (float) $result->latitude,
'longitude' => (float) $result->longitude,
);
}
return null;
}
/**
* Get coordinates for a city from geo table
* Searches for city in MN and IA states (matching MLS_ALLOWED_STATES)
*
* @param string $city The city name to look up
* @return array|null Array with 'latitude' and 'longitude' or null if not found
*/
private function get_city_coordinates($city) {
global $wpdb;
$table = $this->db->geo_cities_table();
// Search in allowed states (MN, IA)
$allowed_states = defined('MLS_ALLOWED_STATES') ? MLS_ALLOWED_STATES : array('MN', 'IA');
$placeholders = implode(',', array_fill(0, count($allowed_states), '%s'));
$values = array_merge(array($city), $allowed_states);
$result = $wpdb->get_row($wpdb->prepare(
"SELECT latitude, longitude FROM {$table} WHERE city = %s AND state_code IN ({$placeholders}) LIMIT 1",
$values
));
if ($result) {
return array(
'latitude' => (float) $result->latitude,
'longitude' => (float) $result->longitude,
);
}
return null;
}
/**
* Build SQL for distance-based filtering using Haversine formula
* Returns properties within specified miles of a center point
*
* @param float $lat Center latitude
* @param float $lng Center longitude
* @param float $miles Radius in miles
* @return string SQL expression for distance filter
*/
private function get_distance_filter_sql($lat, $lng, $miles) {
// Haversine formula: distance in miles
// 3959 is Earth's radius in miles
return sprintf(
"(3959 * acos(cos(radians(%f)) * cos(radians(latitude)) * cos(radians(longitude) - radians(%f)) + sin(radians(%f)) * sin(radians(latitude)))) <= %f",
$lat,
$lng,
$lat,
$miles
);
}
/**
* Get properties matching criteria
*
@@ -65,6 +144,9 @@ class MLS_Query {
'city' => null,
'county' => null,
'postal_code' => null,
'center_lat' => null, // Latitude for radius search
'center_lng' => null, // Longitude for radius search
'radius' => 30, // Miles for radius search (default 30)
'min_price' => null,
'max_price' => null,
'min_beds' => null,
@@ -125,9 +207,50 @@ class MLS_Query {
$values[] = $args['property_type'];
}
// City and postal_code are mutually exclusive - city takes priority
if ($args['city']) {
$where[] = 'city = %s';
$values[] = $args['city'];
// Look up city coordinates for radius search
$city_coords = $this->get_city_coordinates($args['city']);
if ($city_coords) {
// Match exact city OR within 15 miles of city center
$distance_filter = $this->get_distance_filter_sql(
$city_coords['latitude'],
$city_coords['longitude'],
15 // miles
);
$where[] = "(city = %s OR ({$distance_filter}))";
$values[] = $args['city'];
} else {
// Fallback to exact match if city not in geo table
$where[] = 'city = %s';
$values[] = $args['city'];
}
} elseif ($args['postal_code']) {
// Only apply postal_code filter if city is not set
// Look up zip code coordinates for radius search
$zip_coords = $this->get_zipcode_coordinates($args['postal_code']);
if ($zip_coords) {
// Match exact zip code OR within 20 miles of zip code center
$distance_filter = $this->get_distance_filter_sql(
$zip_coords['latitude'],
$zip_coords['longitude'],
20 // miles
);
$where[] = "(postal_code = %s OR ({$distance_filter}))";
$values[] = $args['postal_code'];
} else {
// Fallback to exact match if zip code not in geo table
$where[] = 'postal_code = %s';
$values[] = $args['postal_code'];
}
} elseif ($args['center_lat'] && $args['center_lng']) {
// Direct lat/lng radius search (from homepage location search)
$distance_filter = $this->get_distance_filter_sql(
(float) $args['center_lat'],
(float) $args['center_lng'],
(int) $args['radius']
);
$where[] = "({$distance_filter})";
}
if ($args['county']) {
@@ -135,11 +258,6 @@ class MLS_Query {
$values[] = $args['county'];
}
if ($args['postal_code']) {
$where[] = 'postal_code = %s';
$values[] = $args['postal_code'];
}
if ($args['min_price']) {
$where[] = 'list_price >= %d';
$values[] = (int) $args['min_price'];
@@ -428,9 +546,51 @@ class MLS_Query {
$values[] = $args['property_type'];
}
// City and postal_code are mutually exclusive - city takes priority
if (!empty($args['city'])) {
$where[] = 'city = %s';
$values[] = $args['city'];
// Look up city coordinates for radius search
$city_coords = $this->get_city_coordinates($args['city']);
if ($city_coords) {
// Match exact city OR within 15 miles of city center
$distance_filter = $this->get_distance_filter_sql(
$city_coords['latitude'],
$city_coords['longitude'],
15 // miles
);
$where[] = "(city = %s OR ({$distance_filter}))";
$values[] = $args['city'];
} else {
// Fallback to exact match if city not in geo table
$where[] = 'city = %s';
$values[] = $args['city'];
}
} elseif (!empty($args['postal_code'])) {
// Only apply postal_code filter if city is not set
// Look up zip code coordinates for radius search
$zip_coords = $this->get_zipcode_coordinates($args['postal_code']);
if ($zip_coords) {
// Match exact zip code OR within 20 miles of zip code center
$distance_filter = $this->get_distance_filter_sql(
$zip_coords['latitude'],
$zip_coords['longitude'],
20 // miles
);
$where[] = "(postal_code = %s OR ({$distance_filter}))";
$values[] = $args['postal_code'];
} else {
// Fallback to exact match if zip code not in geo table
$where[] = 'postal_code = %s';
$values[] = $args['postal_code'];
}
} elseif (!empty($args['center_lat']) && !empty($args['center_lng'])) {
// Direct lat/lng radius search (from homepage location search)
$radius = !empty($args['radius']) ? (int) $args['radius'] : 30;
$distance_filter = $this->get_distance_filter_sql(
(float) $args['center_lat'],
(float) $args['center_lng'],
$radius
);
$where[] = "({$distance_filter})";
}
if (!empty($args['county'])) {
@@ -644,14 +804,42 @@ class MLS_Query {
$values[] = $args['property_type'];
}
// City and postal_code are mutually exclusive - city takes priority
if (!empty($args['city'])) {
$where[] = 'city = %s';
$values[] = $args['city'];
}
if (!empty($args['postal_code'])) {
$where[] = 'postal_code = %s';
$values[] = $args['postal_code'];
// Look up city coordinates for radius search
$city_coords = $this->get_city_coordinates($args['city']);
if ($city_coords) {
// Match exact city OR within 15 miles of city center
$distance_filter = $this->get_distance_filter_sql(
$city_coords['latitude'],
$city_coords['longitude'],
15 // miles
);
$where[] = "(city = %s OR ({$distance_filter}))";
$values[] = $args['city'];
} else {
// Fallback to exact match if city not in geo table
$where[] = 'city = %s';
$values[] = $args['city'];
}
} elseif (!empty($args['postal_code'])) {
// Only apply postal_code filter if city is not set
// Look up zip code coordinates for radius search
$zip_coords = $this->get_zipcode_coordinates($args['postal_code']);
if ($zip_coords) {
// Match exact zip code OR within 20 miles of zip code center
$distance_filter = $this->get_distance_filter_sql(
$zip_coords['latitude'],
$zip_coords['longitude'],
20 // miles
);
$where[] = "(postal_code = %s OR ({$distance_filter}))";
$values[] = $args['postal_code'];
} else {
// Fallback to exact match if zip code not in geo table
$where[] = 'postal_code = %s';
$values[] = $args['postal_code'];
}
}
if (!empty($args['min_price'])) {