Fix city/zip filtering and improve map hover behavior

MLS Query Changes:
- Use exact city/postal_code matching instead of radius search
- Fixes city filter returning 1700+ results instead of 97 for Ramsey

Cluster Endpoint:
- Parse "City, SS" format to extract city name before querying
- Fixes pins not showing when city filter applied

Property Filters JS:
- Always fit map bounds when filter changes (not just on no intersection)
- Fit bounds on initial page load when URL has filters
- Show temporary hover pin when marker is clustered or outside viewport
- Uses markerCluster.getVisibleParent() to detect clustered markers

Property Results:
- Add zip code parameter handling for URL filters

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Hanson.xyz Dev
2025-12-29 02:27:54 -06:00
parent 5ca2e29c72
commit f2a9b28ac2
5 changed files with 92 additions and 150 deletions
@@ -209,40 +209,13 @@ class MLS_Query {
// City and postal_code are mutually exclusive - city takes priority // City and postal_code are mutually exclusive - city takes priority
if ($args['city']) { if ($args['city']) {
// Look up city coordinates for radius search // Exact city match
$city_coords = $this->get_city_coordinates($args['city']); $where[] = 'city = %s';
if ($city_coords) { $values[] = $args['city'];
// 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']) { } elseif ($args['postal_code']) {
// Only apply postal_code filter if city is not set // Exact postal code match
// Look up zip code coordinates for radius search $where[] = 'postal_code = %s';
$zip_coords = $this->get_zipcode_coordinates($args['postal_code']); $values[] = $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']) { } elseif ($args['center_lat'] && $args['center_lng']) {
// Direct lat/lng radius search (from homepage location search) // Direct lat/lng radius search (from homepage location search)
$distance_filter = $this->get_distance_filter_sql( $distance_filter = $this->get_distance_filter_sql(
@@ -548,40 +521,13 @@ class MLS_Query {
// City and postal_code are mutually exclusive - city takes priority // City and postal_code are mutually exclusive - city takes priority
if (!empty($args['city'])) { if (!empty($args['city'])) {
// Look up city coordinates for radius search // Exact city match
$city_coords = $this->get_city_coordinates($args['city']); $where[] = 'city = %s';
if ($city_coords) { $values[] = $args['city'];
// 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'])) { } elseif (!empty($args['postal_code'])) {
// Only apply postal_code filter if city is not set // Exact postal code match
// Look up zip code coordinates for radius search $where[] = 'postal_code = %s';
$zip_coords = $this->get_zipcode_coordinates($args['postal_code']); $values[] = $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'])) { } elseif (!empty($args['center_lat']) && !empty($args['center_lng'])) {
// Direct lat/lng radius search (from homepage location search) // Direct lat/lng radius search (from homepage location search)
$radius = !empty($args['radius']) ? (int) $args['radius'] : 30; $radius = !empty($args['radius']) ? (int) $args['radius'] : 30;
@@ -806,40 +752,13 @@ class MLS_Query {
// City and postal_code are mutually exclusive - city takes priority // City and postal_code are mutually exclusive - city takes priority
if (!empty($args['city'])) { if (!empty($args['city'])) {
// Look up city coordinates for radius search // Exact city match
$city_coords = $this->get_city_coordinates($args['city']); $where[] = 'city = %s';
if ($city_coords) { $values[] = $args['city'];
// 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'])) { } elseif (!empty($args['postal_code'])) {
// Only apply postal_code filter if city is not set // Exact postal code match
// Look up zip code coordinates for radius search $where[] = 'postal_code = %s';
$zip_coords = $this->get_zipcode_coordinates($args['postal_code']); $values[] = $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'])) { if (!empty($args['min_price'])) {
+4
View File
@@ -256,6 +256,10 @@ final class MLS_Plugin {
$status = isset($_REQUEST['status']) ? sanitize_text_field($_REQUEST['status']) : 'Active'; $status = isset($_REQUEST['status']) ? sanitize_text_field($_REQUEST['status']) : 'Active';
$property_type = isset($_REQUEST['property_type']) ? sanitize_text_field($_REQUEST['property_type']) : null; $property_type = isset($_REQUEST['property_type']) ? sanitize_text_field($_REQUEST['property_type']) : null;
$city = isset($_REQUEST['city']) ? sanitize_text_field($_REQUEST['city']) : null; $city = isset($_REQUEST['city']) ? sanitize_text_field($_REQUEST['city']) : null;
// Parse "City, SS" format - extract just the city name
if ($city && preg_match('/^(.+),\s*([A-Z]{2})$/', $city, $matches)) {
$city = $matches[1];
}
$min_price = isset($_REQUEST['min_price']) ? (int) $_REQUEST['min_price'] : null; $min_price = isset($_REQUEST['min_price']) ? (int) $_REQUEST['min_price'] : null;
$max_price = isset($_REQUEST['max_price']) ? (int) $_REQUEST['max_price'] : null; $max_price = isset($_REQUEST['max_price']) ? (int) $_REQUEST['max_price'] : null;
$min_beds = isset($_REQUEST['min_beds']) ? (int) $_REQUEST['min_beds'] : null; $min_beds = isset($_REQUEST['min_beds']) ? (int) $_REQUEST['min_beds'] : null;
File diff suppressed because one or more lines are too long
@@ -840,13 +840,36 @@
self.hoveredPropertyId = propertyId; self.hoveredPropertyId = propertyId;
// Check if marker exists on map var marker = self.markers[propertyId];
if (self.markers[propertyId]) { var needsTemporaryMarker = false;
// Marker exists - highlight it
self.setMarkerColor(propertyId, 'blue'); if (marker) {
self.setMarkerZIndex(propertyId, 9000); // Blue below amber but above red // Marker exists - check if it's visible (not clustered and in viewport)
var markerLatLng = marker.getLatLng();
var inViewport = self.map.getBounds().contains(markerLatLng);
// Check if marker is clustered (part of a cluster group)
var isClustered = false;
if (self.markerCluster && self.markerCluster.hasLayer(marker)) {
var visibleParent = self.markerCluster.getVisibleParent(marker);
isClustered = visibleParent && visibleParent !== marker;
}
if (!isClustered && inViewport) {
// Marker is visible as individual pin - highlight it
self.setMarkerColor(propertyId, 'blue');
self.setMarkerZIndex(propertyId, 9000);
} else {
// Marker is clustered or outside viewport - need temporary marker
needsTemporaryMarker = true;
}
} else { } else {
// Marker is clustered - create temporary pin at property location // Marker doesn't exist in current dataset - need temporary marker
needsTemporaryMarker = true;
}
// Create temporary marker if needed
if (needsTemporaryMarker) {
var lat = $card.data('lat'); var lat = $card.data('lat');
var lng = $card.data('lng'); var lng = $card.data('lng');
@@ -981,7 +1004,7 @@
}); });
// City/Zip mutual exclusivity in sticky form - city clears zip // City/Zip mutual exclusivity in sticky form - city clears zip
this.$stickyForm.find('select[name="property_location"]').on('change', function() { this.$stickyForm.find('select[name="city"]').on('change', function() {
if ($(this).val()) { if ($(this).val()) {
self.$stickyForm.find('input[name="zip"]').val(''); self.$stickyForm.find('input[name="zip"]').val('');
self.$mainForm.find('input[name="zip"]').val(''); self.$mainForm.find('input[name="zip"]').val('');
@@ -991,8 +1014,8 @@
// City/Zip mutual exclusivity in sticky form - zip clears city // City/Zip mutual exclusivity in sticky form - zip clears city
this.$stickyForm.find('input[name="zip"]').on('input', function() { this.$stickyForm.find('input[name="zip"]').on('input', function() {
if ($(this).val()) { if ($(this).val()) {
self.$stickyForm.find('select[name="property_location"]').val(''); self.$stickyForm.find('select[name="city"]').val('');
self.$mainForm.find('select[name="property_location"]').val(''); self.$mainForm.find('select[name="city"]').val('');
} }
}); });
@@ -1118,7 +1141,7 @@
}); });
// City/Zip mutual exclusivity - city clears zip // City/Zip mutual exclusivity - city clears zip
this.$form.find('select[name="property_location"]').on('change', function() { this.$form.find('select[name="city"]').on('change', function() {
if ($(this).val()) { if ($(this).val()) {
self.$form.find('input[name="zip"]').val(''); self.$form.find('input[name="zip"]').val('');
// Also sync to sticky form if it exists // Also sync to sticky form if it exists
@@ -1131,10 +1154,10 @@
// City/Zip mutual exclusivity - zip clears city // City/Zip mutual exclusivity - zip clears city
this.$form.find('input[name="zip"]').on('input', function() { this.$form.find('input[name="zip"]').on('input', function() {
if ($(this).val()) { if ($(this).val()) {
self.$form.find('select[name="property_location"]').val(''); self.$form.find('select[name="city"]').val('');
// Also sync to sticky form if it exists // Also sync to sticky form if it exists
if (StickyFilters && StickyFilters.$stickyForm) { if (StickyFilters && StickyFilters.$stickyForm) {
StickyFilters.$stickyForm.find('select[name="property_location"]').val(''); StickyFilters.$stickyForm.find('select[name="city"]').val('');
} }
} }
}); });
@@ -1220,7 +1243,7 @@
PropertyMap.currentFilters = { PropertyMap.currentFilters = {
status: 'Active', status: 'Active',
property_type: formData.property_type || '', property_type: formData.property_type || '',
city: formData.property_location || '', city: formData.city || '',
min_price: formData.min_price || '', min_price: formData.min_price || '',
max_price: formData.max_price || '', max_price: formData.max_price || '',
min_beds: formData.beds || '' min_beds: formData.beds || ''
@@ -1291,7 +1314,7 @@
action: 'homeproz_filter_properties', action: 'homeproz_filter_properties',
nonce: homeprozAjax.nonce, nonce: homeprozAjax.nonce,
property_type: formData.property_type, property_type: formData.property_type,
property_location: formData.property_location, city: formData.city,
zip: formData.zip, zip: formData.zip,
min_price: formData.min_price, min_price: formData.min_price,
max_price: formData.max_price, max_price: formData.max_price,
@@ -1529,7 +1552,7 @@
action: 'homeproz_filter_properties', action: 'homeproz_filter_properties',
nonce: homeprozAjax.nonce, nonce: homeprozAjax.nonce,
property_type: formData.property_type, property_type: formData.property_type,
property_location: formData.property_location, city: formData.city,
zip: formData.zip, zip: formData.zip,
min_price: formData.min_price, min_price: formData.min_price,
max_price: formData.max_price, max_price: formData.max_price,
@@ -1628,7 +1651,7 @@
getFormData: function() { getFormData: function() {
return { return {
property_type: this.$form.find('[name="property_type"]').val() || '', property_type: this.$form.find('[name="property_type"]').val() || '',
property_location: this.$form.find('[name="property_location"]').val() || '', city: this.$form.find('[name="city"]').val() || '',
zip: this.$form.find('[name="zip"]').val() || '', zip: this.$form.find('[name="zip"]').val() || '',
min_price: this.$form.find('[name="min_price"]').val() || '', min_price: this.$form.find('[name="min_price"]').val() || '',
max_price: this.$form.find('[name="max_price"]').val() || '', max_price: this.$form.find('[name="max_price"]').val() || '',
@@ -1729,7 +1752,7 @@
return { return {
// Filters // Filters
property_type: raw.property_type || '', property_type: raw.property_type || '',
property_location: raw.property_location || '', city: raw.city || '',
zip: raw.zip || '', zip: raw.zip || '',
min_price: raw.min_price || '', min_price: raw.min_price || '',
max_price: raw.max_price || '', max_price: raw.max_price || '',
@@ -1792,7 +1815,7 @@
PropertyMap.currentFilters = { PropertyMap.currentFilters = {
status: 'Active', status: 'Active',
property_type: formData.property_type || '', property_type: formData.property_type || '',
city: formData.property_location || '', city: formData.city || '',
min_price: formData.min_price || '', min_price: formData.min_price || '',
max_price: formData.max_price || '', max_price: formData.max_price || '',
min_beds: formData.beds || '' min_beds: formData.beds || ''
@@ -1812,7 +1835,7 @@
data: { data: {
action: 'homeproz_get_filter_bounds', action: 'homeproz_get_filter_bounds',
property_type: formData.property_type, property_type: formData.property_type,
city: formData.property_location, city: formData.city,
min_price: formData.min_price, min_price: formData.min_price,
max_price: formData.max_price, max_price: formData.max_price,
min_beds: formData.beds min_beds: formData.beds
@@ -1820,35 +1843,19 @@
success: function(response) { success: function(response) {
if (response.success && response.data) { if (response.success && response.data) {
var filterBounds = response.data; var filterBounds = response.data;
var mapBounds = PropertyMap.map.getBounds();
// Check if map view intersects with filter bounds // Add 10% padding to bounds
var filterLatLngBounds = L.latLngBounds( var latPadding = (filterBounds.ne_lat - filterBounds.sw_lat) * 0.1;
[filterBounds.sw_lat, filterBounds.sw_lng], var lngPadding = (filterBounds.ne_lng - filterBounds.sw_lng) * 0.1;
[filterBounds.ne_lat, filterBounds.ne_lng]
var paddedBounds = L.latLngBounds(
[filterBounds.sw_lat - latPadding, filterBounds.sw_lng - lngPadding],
[filterBounds.ne_lat + latPadding, filterBounds.ne_lng + lngPadding]
); );
// Check if ANY of the filter bounds corners are visible in the map // Always fit map to show all filtered properties
// OR if the map view is fully contained within the filter bounds PropertyMap.map.fitBounds(paddedBounds);
var mapIntersects = mapBounds.intersects(filterLatLngBounds); // The moveend event will trigger loadClusters and updateFromMap
if (!mapIntersects) {
// Add 10% padding to bounds
var latPadding = (filterBounds.ne_lat - filterBounds.sw_lat) * 0.1;
var lngPadding = (filterBounds.ne_lng - filterBounds.sw_lng) * 0.1;
var paddedBounds = L.latLngBounds(
[filterBounds.sw_lat - latPadding, filterBounds.sw_lng - lngPadding],
[filterBounds.ne_lat + latPadding, filterBounds.ne_lng + lngPadding]
);
// Reposition map to show filtered properties
PropertyMap.map.fitBounds(paddedBounds);
// The moveend event will trigger loadClusters and updateFromMap
} else {
// Map already shows relevant area, just reload clusters and properties
PropertyMap.loadClusters();
}
} else { } else {
// No properties found with these filters, just reload // No properties found with these filters, just reload
PropertyMap.loadClusters(); PropertyMap.loadClusters();
@@ -1890,7 +1897,7 @@
var mapFilters = { var mapFilters = {
status: 'Active', status: 'Active',
property_type: formData.property_type || '', property_type: formData.property_type || '',
city: formData.property_location || '', city: formData.city || '',
min_price: formData.min_price || '', min_price: formData.min_price || '',
max_price: formData.max_price || '', max_price: formData.max_price || '',
min_beds: formData.beds || '' min_beds: formData.beds || ''
@@ -1901,6 +1908,9 @@
// Restore state if we have pending state to restore // Restore state if we have pending state to restore
if (PropertyFilters.pendingRestoreState) { if (PropertyFilters.pendingRestoreState) {
PropertyFilters.restoreState(); PropertyFilters.restoreState();
} else if (formData.city || formData.zip || formData.property_type || formData.min_price || formData.max_price || formData.beds) {
// Filters present from URL but no saved map state - fit to filter bounds
PropertyFilters.onFilterChange();
} }
} }
@@ -1940,7 +1950,7 @@
var mapFilters = { var mapFilters = {
status: 'Active', status: 'Active',
property_type: formData.property_type || '', property_type: formData.property_type || '',
city: formData.property_location || '', city: formData.city || '',
min_price: formData.min_price || '', min_price: formData.min_price || '',
max_price: formData.max_price || '', max_price: formData.max_price || '',
min_beds: formData.beds || '' min_beds: formData.beds || ''
@@ -2480,7 +2490,7 @@
action: 'homeproz_filter_properties', action: 'homeproz_filter_properties',
nonce: homeprozAjax.nonce, nonce: homeprozAjax.nonce,
property_type: formData.property_type, property_type: formData.property_type,
property_location: formData.property_location, city: formData.city,
zip: formData.zip, zip: formData.zip,
min_price: formData.min_price, min_price: formData.min_price,
max_price: formData.max_price, max_price: formData.max_price,
@@ -26,7 +26,8 @@ if (!function_exists('mls_get_properties')) {
// Get filter values from URL // Get filter values from URL
$current_type = isset($_GET['property_type']) ? sanitize_text_field($_GET['property_type']) : ''; $current_type = isset($_GET['property_type']) ? sanitize_text_field($_GET['property_type']) : '';
$current_status = isset($_GET['property_status']) ? sanitize_text_field($_GET['property_status']) : 'Active'; $current_status = isset($_GET['property_status']) ? sanitize_text_field($_GET['property_status']) : 'Active';
$current_location = isset($_GET['property_location']) ? sanitize_text_field($_GET['property_location']) : ''; $current_location = isset($_GET['city']) ? sanitize_text_field($_GET['city']) : '';
$current_zip = isset($_GET['zip']) ? sanitize_text_field($_GET['zip']) : '';
$current_min_price = isset($_GET['min_price']) ? intval($_GET['min_price']) : ''; $current_min_price = isset($_GET['min_price']) ? intval($_GET['min_price']) : '';
$current_max_price = isset($_GET['max_price']) ? intval($_GET['max_price']) : ''; $current_max_price = isset($_GET['max_price']) ? intval($_GET['max_price']) : '';
$current_beds = isset($_GET['beds']) ? intval($_GET['beds']) : ''; $current_beds = isset($_GET['beds']) ? intval($_GET['beds']) : '';
@@ -49,7 +50,15 @@ if ($current_type) {
$filter_args['property_type'] = $current_type; $filter_args['property_type'] = $current_type;
} }
if ($current_location) { if ($current_location) {
$filter_args['city'] = $current_location; // Parse "City, SS" format - extract just the city name
$city_name = $current_location;
if (preg_match('/^(.+),\s*([A-Z]{2})$/', $current_location, $matches)) {
$city_name = $matches[1];
}
$filter_args['city'] = $city_name;
} elseif ($current_zip) {
// Zip code filter (only if city not set)
$filter_args['postal_code'] = $current_zip;
} }
// Use lat/lng radius search if provided (takes precedence over location) // Use lat/lng radius search if provided (takes precedence over location)
if ($center_lat && $center_lng) { if ($center_lat && $center_lng) {