geohash編碼和周邊查詢

<?php
/**
 * 生成指定區(qū)域內的隨機經緯度(以北京區(qū)域為例,方便驗證效果)
 * 北京大致范圍:緯度39.4°~41.6°,經度115.4°~117.8°
 * @return array [lat, lng] 隨機緯度、經度
 */
function generate_random_lnglat() {
    // 隨機緯度(39.4 ~ 41.6)
    $lat = 39.4 + mt_rand(0, 220000) / 100000;
    // 隨機經度(115.4 ~ 117.8)
    $lng = 115.4 + mt_rand(0, 240000) / 100000;
    // 保留6位小數(shù)(與數(shù)據(jù)表字段一致)
    return [
        round($lat, 6),
        round($lng, 6)
    ];
}

/**
 * GeoHash編碼核心函數(shù):將經緯度轉換為GeoHash字符串
 * GeoHash原理:將地球表面劃分為網格,每個網格用一串字符表示,字符越長精度越高
 * @param float $lat 緯度(-90 ~ 90)
 * @param float $lng 經度(-180 ~ 180)
 * @param int $precision GeoHash精度(默認8位,6位約對應10km范圍,8位約對應19m范圍)
 * @return string 生成的GeoHash字符串
 */
function geohash_encode($lat, $lng, $precision = 8) {
    $base32 = '0123456789bcdefghjkmnpqrstuvwxyz';
    $latRange = [-90.0, 90.0];
    $lngRange = [-180.0, 180.0];
    $binary = '';

    while (strlen($binary) < $precision * 5) {
        $lngMid = ($lngRange[0] + $lngRange[1]) / 2;
        if ($lng > $lngMid) {
            $binary .= '1';
            $lngRange[0] = $lngMid;
        } else {
            $binary .= '0';
            $lngRange[1] = $lngMid;
        }

        $latMid = ($latRange[0] + $latRange[1]) / 2;
        if ($lat > $latMid) {
            $binary .= '1';
            $latRange[0] = $latMid;
        } else {
            $binary .= '0';
            $latRange[1] = $latMid;
        }
    }

    $geohash = '';
    for ($i = 0; $i < strlen($binary); $i += 5) {
        $chunk = substr($binary, $i, 5);
        $index = bindec($chunk);
        $geohash .= $base32[$index];
    }

    return substr($geohash, 0, $precision);
}

/**
 * 計算6位GeoHash的8個相鄰區(qū)域(上、右上、右、右下、下、左下、左、左上)
 * 6位GeoHash是常用精度,對應約1.2km×0.6km的矩形區(qū)域
 * @param string $geohash6 6位的GeoHash字符串
 * @return array 包含自身和8個鄰居的GeoHash數(shù)組(共9個)
 */
function geohash_get_6bit_neighbors($geohash6) {
    $neighbors = [
        'north'     => ['p0r21436x8zb9dcf5h7kjnmqesgutwvy', 'bc01fg45238967deuvhjyznpkmstqrwx'],
        'northeast' => ['14365h7k9dcfesgujnmqp0r2twvyx8zb', '238967debc01fg45kmstqrwxuvhjyznp'],
        'east'      => ['bc01fg45238967deuvhjyznpkmstqrwx', 'p0r21436x8zb9dcf5h7kjnmqesgutwvy'],
        'southeast' => ['238967debc01fg45kmstqrwxuvhjyznp', '14365h7k9dcfesgujnmqp0r2twvyx8zb'],
        'south'     => ['14365h7k9dcfesgujnmqp0r2twvyx8zb', '238967debc01fg45kmstqrwxuvhjyznp'],
        'southwest' => ['p0r21436x8zb9dcf5h7kjnmqesgutwvy', 'bc01fg45238967deuvhjyznpkmstqrwx'],
        'west'      => ['bc01fg45238967deuvhjyznpkmstqrwx', 'p0r21436x8zb9dcf5h7kjnmqesgutwvy'],
        'northwest' => ['238967debc01fg45kmstqrwxuvhjyznp', '14365h7k9dcfesgujnmqp0r2twvyx8zb']
    ];
    $borders = [
        'north'     => ['prxz', 'bcfguvyz'],
        'northeast' => ['prxz', 'bcfguvyz'],
        'east'      => ['bcfguvyz', 'prxz'],
        'southeast' => ['bcfguvyz', 'prxz'],
        'south'     => ['028b', '0145hjnp'],
        'southwest' => ['028b', '0145hjnp'],
        'west'      => ['0145hjnp', '028b'],
        'northwest' => ['0145hjnp', '028b']
    ];

    $geohash = strtolower($geohash6);
    $len = strlen($geohash);
    if ($len !== 6) {
        return [$geohash6];
    }
    $base32 = '0123456789bcdefghjkmnpqrstuvwxyz';
    $base32Idx = array_flip(str_split($base32));
    $neighborsList = [$geohash6];

    // 遍歷8個方向
    $directions = ['north', 'northeast', 'east', 'southeast', 'south', 'southwest', 'west', 'northwest'];
    foreach ($directions as $dir) {
        $neighbor = geohash_calculate_neighbor($geohash, $dir, $neighbors, $borders, $base32, $base32Idx);
        if ($neighbor && !in_array($neighbor, $neighborsList)) {
            $neighborsList[] = $neighbor;
        }
    }
    return $neighborsList;
}

/**
 * 遞歸計算單個方向的GeoHash鄰居(核心輔助函數(shù))
 * @param string $hash 原始GeoHash字符串
 * @param string $dir 方向(north/east/south/west等)
 * @param array $neighbors 鄰居字符映射表
 * @param array $borders 邊界字符表
 * @param string $base32 32進制編碼表
 * @param array $base32Idx 32進制字符索引映射
 * @return string|null 計算出的鄰居GeoHash,越界則返回null
 */
function geohash_calculate_neighbor($hash, $dir, $neighbors, $borders, $base32, $base32Idx) {
    $len = strlen($hash);
    $last = substr($hash, -1);
    $prefix = substr($hash, 0, $len - 1);
    $isLat = ($len % 2) == 1; // true: 最后一位是緯度

    // 檢查是否在邊界,需要遞歸進位
    $borderChars = $borders[$dir][$isLat ? 1 : 0];
    if (in_array($last, str_split($borderChars)) && $prefix !== '') {
        $prefix = geohash_calculate_neighbor($prefix, $dir, $neighbors, $borders, $base32, $base32Idx);
        if ($prefix === null) {
            return null; // 越界,無鄰居
        }
    }

    // 獲取原字符在 base32 中的位置
    if (!isset($base32Idx[$last])) {
        return null; // 非法字符
    }
    $idx = $base32Idx[$last];

    // 從 neighbor 映射表中取對應字符
    $neighborMap = $neighbors[$dir][$isLat ? 1 : 0];
    if ($idx >= strlen($neighborMap)) {
        return null; // 安全檢查
    }

    $newLast = $neighborMap[$idx];
    return $prefix . $newLast;
}

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容