├── .gitignore ├── src ├── test.php └── geohash.class.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | -------------------------------------------------------------------------------- /src/test.php: -------------------------------------------------------------------------------- 1 | encode(39.98123848, 116.30683690); 17 | //取前缀,前缀约长范围越小 18 | $prefix = substr($hash, 0, 6); 19 | //取出相邻八个区域 20 | $neighbors = $geohash->neighbors($prefix); 21 | array_push($neighbors, $prefix); 22 | 23 | print_r($neighbors); 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | geohash 2 | ============ 3 | 4 | 这年头和LBS相关的应用越来越火. 从foursquare的热闹程度就可见一般, 更不用说微信、陌陌了 (什么, 没听过 foursquare... 哥们, 你 out 了). 和 LBS有关的应用一般都包括一些共同的操作, 最常见的一个, 就是找附近的东东(餐馆, 商店, 妞....). 所以, 这里就抛出了一个问题, 怎样才能在大量经纬度数据中检索出附近的点呢? 5 | 6 | geohash能做到 [@一个开发者](http://weibo.com/smcz) 7 | 8 | [![](http://service.t.sina.com.cn/widget/qmd/1656360925/02781ba4/4.png)](http://weibo.com/smcz) 9 | 10 | [![paypaldonate](https://www.paypalobjects.com/en_GB/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=VW7YCWNMQ7ZXY) 11 | 12 | ![](http://image.sinastorage.com/donate.png) 13 | 14 | 15 | ### Requirements 16 | 17 | * PHP >= 4 18 | 19 | ### Usage 20 | 21 | 22 | - 例如: 用iPhone/android手机定位得到理想国际大厦的经纬度: 39.98123848, 116.30683690 然后查找附近的妞 ![](http://www.sinaimg.cn/uc/myshow/blog/misc/gif/E___6715EN00SIGG.gif) 23 | 24 | ```php 25 | 26 | require_once('geohash.class.php'); 27 | $geohash = new Geohash; 28 | //得到这点的hash值 29 | $hash = $geohash->encode(39.98123848, 116.30683690); 30 | //取前缀,前缀约长范围越小 31 | $prefix = substr($hash, 0, 6); 32 | //取出相邻八个区域 33 | $neighbors = $geohash->neighbors($prefix); 34 | array_push($neighbors, $prefix); 35 | 36 | print_r($neighbors); 37 | 38 | ``` 39 | 40 | - 得到9个geohash值 41 | 42 | ```php 43 | 44 | //得到9个geohash值 45 | 46 | Array 47 | ( 48 | [top] => wx4eqx 49 | [bottom] => wx4eqt 50 | [right] => wx4eqy 51 | [left] => wx4eqq 52 | [topleft] => wx4eqr 53 | [topright] => wx4eqz 54 | [bottomright] => wx4eqv 55 | [bottomleft] => wx4eqm 56 | [0] => wx4eqw 57 | ) 58 | 59 | ``` 60 | 61 | - 范围如图: 62 | 63 | ![](http://s15.sinaimg.cn/orignal/62ba0fddtab3b8381ce8e&690) 64 | 65 | 66 | 67 | - 用sql语句查询 68 | 69 | ```sql 70 | SELECT * FROM xy WHERE geohash LIKE 'wx4eqw%'; 71 | SELECT * FROM xy WHERE geohash LIKE 'wx4eqx%'; 72 | SELECT * FROM xy WHERE geohash LIKE 'wx4eqt%'; 73 | SELECT * FROM xy WHERE geohash LIKE 'wx4eqy%'; 74 | SELECT * FROM xy WHERE geohash LIKE 'wx4eqq%'; 75 | SELECT * FROM xy WHERE geohash LIKE 'wx4eqr%'; 76 | SELECT * FROM xy WHERE geohash LIKE 'wx4eqz%'; 77 | SELECT * FROM xy WHERE geohash LIKE 'wx4eqv%'; 78 | SELECT * FROM xy WHERE geohash LIKE 'wx4eqm%'; 79 | ``` 80 | 81 | - 看一下是否用上索引 (一共有50多万行测试数据): 82 | 83 | 索引: 84 | 85 | ![](http://s15.sinaimg.cn/orignal/62ba0fddtab3b8463f9ce&690) 86 | 87 | 数据: 88 | 89 | ![](http://s1.sinaimg.cn/orignal/62ba0fddtab3b84d6c250&690) 90 | 91 | ```sql 92 | EXPLAIN SELECT * FROM xy WHERE geohash LIKE 'wx4eqw%'; 93 | ``` 94 | 95 | ![](http://s8.sinaimg.cn/orignal/62ba0fddtab3b86ca9007&690) 96 | 97 | 98 | 其他资料: 99 | - geohash演示: http://openlocation.org/geohash/geohash-js/ 100 | - wiki: http://en.wikipedia.org/wiki/Geohash 101 | - 原理: https://github.com/CloudSide/geohash/wiki 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/geohash.class.php: -------------------------------------------------------------------------------- 1 | neighbors['right']['even'] = 'bc01fg45238967deuvhjyznpkmstqrwx'; 32 | $this->neighbors['left']['even'] = '238967debc01fg45kmstqrwxuvhjyznp'; 33 | $this->neighbors['top']['even'] = 'p0r21436x8zb9dcf5h7kjnmqesgutwvy'; 34 | $this->neighbors['bottom']['even'] = '14365h7k9dcfesgujnmqp0r2twvyx8zb'; 35 | 36 | $this->borders['right']['even'] = 'bcfguvyz'; 37 | $this->borders['left']['even'] = '0145hjnp'; 38 | $this->borders['top']['even'] = 'prxz'; 39 | $this->borders['bottom']['even'] = '028b'; 40 | 41 | $this->neighbors['bottom']['odd'] = $this->neighbors['left']['even']; 42 | $this->neighbors['top']['odd'] = $this->neighbors['right']['even']; 43 | $this->neighbors['left']['odd'] = $this->neighbors['bottom']['even']; 44 | $this->neighbors['right']['odd'] = $this->neighbors['top']['even']; 45 | 46 | $this->borders['bottom']['odd'] = $this->borders['left']['even']; 47 | $this->borders['top']['odd'] = $this->borders['right']['even']; 48 | $this->borders['left']['odd'] = $this->borders['bottom']['even']; 49 | $this->borders['right']['odd'] = $this->borders['top']['even']; 50 | 51 | //build map from encoding char to 0 padded bitfield 52 | for($i=0; $i<32; $i++) { 53 | 54 | $this->codingMap[substr($this->coding, $i, 1)] = str_pad(decbin($i), 5, "0", STR_PAD_LEFT); 55 | } 56 | 57 | } 58 | 59 | /** 60 | * Decode a geohash and return an array with decimal lat,long in it 61 | * Author: Bruce Chen (weibo: @一个开发者) 62 | */ 63 | public function decode($hash) { 64 | 65 | //decode hash into binary string 66 | $binary = ""; 67 | $hl = strlen($hash); 68 | for ($i=0; $i<$hl; $i++) { 69 | 70 | $binary .= $this->codingMap[substr($hash, $i, 1)]; 71 | } 72 | 73 | //split the binary into lat and log binary strings 74 | $bl = strlen($binary); 75 | $blat = ""; 76 | $blong = ""; 77 | for ($i=0; $i<$bl; $i++) { 78 | 79 | if ($i%2) 80 | $blat=$blat.substr($binary, $i, 1); 81 | else 82 | $blong=$blong.substr($binary, $i, 1); 83 | 84 | } 85 | 86 | //now concert to decimal 87 | $lat = $this->binDecode($blat, -90, 90); 88 | $long = $this->binDecode($blong, -180, 180); 89 | 90 | //figure out how precise the bit count makes this calculation 91 | $latErr = $this->calcError(strlen($blat), -90, 90); 92 | $longErr = $this->calcError(strlen($blong), -180, 180); 93 | 94 | //how many decimal places should we use? There's a little art to 95 | //this to ensure I get the same roundings as geohash.org 96 | $latPlaces = max(1, -round(log10($latErr))) - 1; 97 | $longPlaces = max(1, -round(log10($longErr))) - 1; 98 | 99 | //round it 100 | $lat = round($lat, $latPlaces); 101 | $long = round($long, $longPlaces); 102 | 103 | return array($lat, $long); 104 | } 105 | 106 | 107 | private function calculateAdjacent($srcHash, $dir) { 108 | 109 | $srcHash = strtolower($srcHash); 110 | $lastChr = $srcHash[strlen($srcHash) - 1]; 111 | $type = (strlen($srcHash) % 2) ? 'odd' : 'even'; 112 | $base = substr($srcHash, 0, strlen($srcHash) - 1); 113 | 114 | if (strpos($this->borders[$dir][$type], $lastChr) !== false) { 115 | 116 | $base = $this->calculateAdjacent($base, $dir); 117 | } 118 | 119 | return $base . $this->coding[strpos($this->neighbors[$dir][$type], $lastChr)]; 120 | } 121 | 122 | 123 | public function neighbors($srcHash) { 124 | 125 | $geohashPrefix = substr($srcHash, 0, strlen($srcHash) - 1); 126 | 127 | $neighbors['top'] = $this->calculateAdjacent($srcHash, 'top'); 128 | $neighbors['bottom'] = $this->calculateAdjacent($srcHash, 'bottom'); 129 | $neighbors['right'] = $this->calculateAdjacent($srcHash, 'right'); 130 | $neighbors['left'] = $this->calculateAdjacent($srcHash, 'left'); 131 | 132 | $neighbors['topleft'] = $this->calculateAdjacent($neighbors['left'], 'top'); 133 | $neighbors['topright'] = $this->calculateAdjacent($neighbors['right'], 'top'); 134 | $neighbors['bottomright'] = $this->calculateAdjacent($neighbors['right'], 'bottom'); 135 | $neighbors['bottomleft'] = $this->calculateAdjacent($neighbors['left'], 'bottom'); 136 | 137 | return $neighbors; 138 | } 139 | 140 | 141 | 142 | /** 143 | * Encode a hash from given lat and long 144 | * Author: Bruce Chen (weibo: @一个开发者) 145 | */ 146 | public function encode($lat, $long) { 147 | 148 | //how many bits does latitude need? 149 | $plat = $this->precision($lat); 150 | $latbits = 1; 151 | $err = 45; 152 | while($err > $plat) { 153 | 154 | $latbits++; 155 | $err /= 2; 156 | } 157 | 158 | //how many bits does longitude need? 159 | $plong = $this->precision($long); 160 | $longbits = 1; 161 | $err = 90; 162 | while($err > $plong) { 163 | 164 | $longbits++; 165 | $err /= 2; 166 | } 167 | 168 | //bit counts need to be equal 169 | $bits = max($latbits, $longbits); 170 | 171 | //as the hash create bits in groups of 5, lets not 172 | //waste any bits - lets bulk it up to a multiple of 5 173 | //and favour the longitude for any odd bits 174 | $longbits = $bits; 175 | $latbits = $bits; 176 | $addlong = 1; 177 | while (($longbits + $latbits) % 5 != 0) { 178 | 179 | $longbits += $addlong; 180 | $latbits += !$addlong; 181 | $addlong = !$addlong; 182 | } 183 | 184 | 185 | //encode each as binary string 186 | $blat = $this->binEncode($lat, -90, 90, $latbits); 187 | $blong = $this->binEncode($long, -180, 180, $longbits); 188 | 189 | //merge lat and long together 190 | $binary = ""; 191 | $uselong = 1; 192 | while (strlen($blat) + strlen($blong)) { 193 | 194 | if ($uselong) { 195 | 196 | $binary = $binary.substr($blong, 0, 1); 197 | $blong = substr($blong, 1); 198 | 199 | } else { 200 | 201 | $binary = $binary.substr($blat, 0, 1); 202 | $blat = substr($blat, 1); 203 | } 204 | 205 | $uselong = !$uselong; 206 | } 207 | 208 | //convert binary string to hash 209 | $hash = ""; 210 | for ($i=0; $icoding[$n]; 214 | } 215 | 216 | return $hash; 217 | } 218 | 219 | /** 220 | * What's the maximum error for $bits bits covering a range $min to $max 221 | */ 222 | private function calcError($bits, $min, $max) { 223 | 224 | $err = ($max - $min) / 2; 225 | while ($bits--) 226 | $err /= 2; 227 | return $err; 228 | } 229 | 230 | /* 231 | * returns precision of number 232 | * precision of 42 is 0.5 233 | * precision of 42.4 is 0.05 234 | * precision of 42.41 is 0.005 etc 235 | * 236 | * Author: Bruce Chen (weibo: @一个开发者) 237 | */ 238 | private function precision($number) { 239 | 240 | $precision = 0; 241 | $pt = strpos($number,'.'); 242 | if ($pt !== false) { 243 | 244 | $precision = -(strlen($number) - $pt - 1); 245 | } 246 | 247 | return pow(10, $precision) / 2; 248 | } 249 | 250 | 251 | /** 252 | * create binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example 253 | * removing the tail recursion is left an exercise for the reader 254 | * 255 | * Author: Bruce Chen (weibo: @一个开发者) 256 | */ 257 | private function binEncode($number, $min, $max, $bitcount) { 258 | 259 | if ($bitcount == 0) 260 | return ""; 261 | 262 | #echo "$bitcount: $min $max
"; 263 | 264 | //this is our mid point - we will produce a bit to say 265 | //whether $number is above or below this mid point 266 | $mid = ($min + $max) / 2; 267 | if ($number > $mid) 268 | return "1" . $this->binEncode($number, $mid, $max, $bitcount - 1); 269 | else 270 | return "0" . $this->binEncode($number, $min, $mid, $bitcount - 1); 271 | } 272 | 273 | 274 | /** 275 | * decodes binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example 276 | * removing the tail recursion is left an exercise for the reader 277 | * 278 | * Author: Bruce Chen (weibo: @一个开发者) 279 | */ 280 | private function binDecode($binary, $min, $max) { 281 | 282 | $mid = ($min + $max) / 2; 283 | 284 | if (strlen($binary) == 0) 285 | return $mid; 286 | 287 | $bit = substr($binary, 0, 1); 288 | $binary = substr($binary, 1); 289 | 290 | if ($bit == 1) 291 | return $this->binDecode($binary, $mid, $max); 292 | else 293 | return $this->binDecode($binary, $min, $mid); 294 | } 295 | } 296 | 297 | 298 | 299 | 300 | 301 | --------------------------------------------------------------------------------