geohash编码和周边查询

<?php
/**
 * 生成指定区域内的随机经纬度(以北京区域为例,方便验证效果)
 * 北京大致范围:纬度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位小数(与数据表字段一致)
    return [
        round($lat, 6),
        round($lng, 6)
    ];
}

/**
 * GeoHash编码核心函数:将经纬度转换为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个相邻区域(上、右上、右、右下、下、左下、左、左上)
 * 6位GeoHash是常用精度,对应约1.2km×0.6km的矩形区域
 * @param string $geohash6 6位的GeoHash字符串
 * @return array 包含自身和8个邻居的GeoHash数组(共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邻居(核心辅助函数)
 * @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;
}

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容