Fix clustering density with pixel-based grid calculation

Replace geohash precision mapping with dynamic grid sizing based on
target pixel spacing (60px between cluster centers). Uses Leaflet/OSM
tile math to calculate degrees-per-pixel at each zoom level, adjusted
for Mercator projection at the viewport's center latitude.

At zoom 7, this gives ~52km cells and ~150 clusters statewide,
properly separating Minneapolis from St. Cloud.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hanson.xyz Dev
2025-12-16 00:31:11 -06:00
parent ae0fc65e4e
commit 1c728ec60e
@@ -18,29 +18,10 @@ class MLS_Cluster {
const GEOHASH_CHARS = '0123456789bcdefghjkmnpqrstuvwxyz';
/**
* Zoom level to geohash precision mapping
* Lower precision = larger geographic area = more clustering
* Target pixel spacing between cluster centers
* With 30px cluster icons, 45px gap gives 75px total spacing
*/
const ZOOM_PRECISION = array(
3 => 2, // ~630km cells - continent level
4 => 2,
5 => 2,
6 => 3, // ~78km cells - state level
7 => 3,
8 => 3,
9 => 4, // ~20km cells - county level
10 => 4,
11 => 4,
12 => 5, // ~2.4km cells - city level
13 => 5,
14 => 5,
15 => 6, // ~610m cells - neighborhood level
16 => 6,
17 => 6,
18 => 7, // ~76m cells - street level
19 => 7,
20 => 8, // Individual points
);
const CLUSTER_PIXEL_SPACING = 60;
/**
* Maximum properties to return as individual markers
@@ -156,20 +137,32 @@ class MLS_Cluster {
}
/**
* Get precision for zoom level
* Calculate grid cell size in degrees for a given zoom level
*
* Uses Leaflet/OSM tile math to determine what geographic distance
* corresponds to CLUSTER_PIXEL_SPACING pixels at the given zoom.
*
* @param int $zoom Map zoom level (1-20)
* @return int Geohash precision
* @param float $lat Center latitude (affects Mercator projection)
* @return array [lat_step, lng_step] in degrees
*/
public function get_precision_for_zoom($zoom) {
public function get_grid_step_for_zoom($zoom, $lat = 45.0) {
$zoom = max(3, min(20, (int) $zoom));
if (isset(self::ZOOM_PRECISION[$zoom])) {
return self::ZOOM_PRECISION[$zoom];
}
// Degrees per pixel at zoom level (longitude)
// 360 degrees / (256 pixels * 2^zoom tiles)
$degrees_per_pixel_lng = 360.0 / (256 * pow(2, $zoom));
// Default to precision 5 for unknown zoom levels
return 5;
// Latitude degrees per pixel (adjusted for Mercator at given latitude)
// At the equator it's the same, at poles it's compressed
$lat_rad = deg2rad(abs($lat));
$degrees_per_pixel_lat = $degrees_per_pixel_lng * cos($lat_rad);
// Calculate step size to achieve target pixel spacing
$lng_step = self::CLUSTER_PIXEL_SPACING * $degrees_per_pixel_lng;
$lat_step = self::CLUSTER_PIXEL_SPACING * $degrees_per_pixel_lat;
return array($lat_step, $lng_step);
}
/**
@@ -258,14 +251,18 @@ class MLS_Cluster {
}
// If low count or high zoom, return individual markers
$precision = $this->get_precision_for_zoom($args['zoom']);
if ($total <= self::MAX_INDIVIDUAL_MARKERS || $args['zoom'] >= 18) {
if ($total <= self::MAX_INDIVIDUAL_MARKERS || $args['zoom'] >= 16) {
return $this->get_individual_markers($where_sql, $values, $total);
}
// Calculate center latitude for Mercator adjustment
$center_lat = 45.0; // Default Minnesota
if ($args['bounds'] && count($args['bounds']) === 4) {
$center_lat = ($args['bounds'][0] + $args['bounds'][2]) / 2;
}
// Return clusters
return $this->get_cluster_data($where_sql, $values, $precision, $total);
return $this->get_cluster_data($where_sql, $values, $args['zoom'], $center_lat, $total);
}
/**
@@ -334,27 +331,22 @@ class MLS_Cluster {
}
/**
* Get clustered data using geohash grouping
* Get clustered data using grid-based grouping
*
* @param string $where_sql WHERE clause
* @param array $values Prepared values
* @param int $precision Geohash precision
* @param int $zoom Map zoom level
* @param float $center_lat Center latitude for Mercator adjustment
* @param int $total Total count
* @return array
*/
private function get_cluster_data($where_sql, $values, $precision, $total) {
private function get_cluster_data($where_sql, $values, $zoom, $center_lat, $total) {
global $wpdb;
$table = $this->db->properties_table();
// Use MySQL SUBSTR to extract geohash prefix for grouping
// We compute geohash on-the-fly using latitude/longitude
// For better performance with 30k+ records, we use grid-based clustering
// Calculate grid cell size based on precision
// Approximate degrees per geohash precision level
$lat_step = 180 / pow(2, (int)($precision * 2.5));
$lng_step = 360 / pow(2, (int)($precision * 2.5));
// Calculate grid cell size based on zoom level and target pixel spacing
list($lat_step, $lng_step) = $this->get_grid_step_for_zoom($zoom, $center_lat);
// Use FLOOR to group coordinates into cells
$sql = "SELECT
@@ -388,7 +380,8 @@ class MLS_Cluster {
'type' => 'clusters',
'total' => $total,
'cluster_count' => count($clusters),
'precision' => $precision,
'zoom' => $zoom,
'grid_size_deg' => array('lat' => $lat_step, 'lng' => $lng_step),
'clusters' => $clusters,
);
}