經過一系列的溝通下來,可以通過geohash的方案來解決這個問題。
基本流程可以是這樣:
(1)原始詳細地址數據--->經緯度數值--->geohash字符串編碼--->數據冗餘保存,主鍵換爲geohash,然後原始數據後置保存
(2)請求接口,參數爲詳細地址,詳細地址進行轉換成geohash,然後基於geohash編碼來進行搜索和排序,返回結果
詳細地址轉換爲經緯度這個可以直接調取成熟的geocoding服務來進行解決,地址規範的情況下,定位到街道應該不會很大,雖然有時候有一定的偏差,但是民用的話基本可以接受呵呵。
所以目前的流程的話卡在了geohash算法這裏,所以寫這篇文章詳細的介紹一下。
geohash的最簡單解釋:將一個經緯度信息,轉換成一個可排序、可比較的字符串編碼。
將經緯度的信息,按照(-90,90)(-180,180)來轉換成平面座標系。
借用一篇文章中的例子來說明一下編碼生成的過程:
首先將緯度範圍(-90, 90)平分成兩個區間(-90, 0)、(0, 90),如果目標緯度位於前一個區間,則編碼爲0,否則編碼爲1。
由於39.92324屬於(0, 90),所以取編碼爲1。
然後再將(0, 90)分成 (0, 45), (45, 90)兩個區間,而39.92324位於(0, 45),所以編碼爲0。
以此類推,直到精度符合要求爲止,得到緯度編碼爲1011 1000 1100 0111 1001。
經度也用同樣的算法,對(-180, 180)依次細分,得到116.3906的編碼爲1101 0010 1100 0100 0100。
接下來將經度和緯度的編碼合併,奇數位是緯度,偶數位是經度,得到編碼 11100 11101 00100 01111 00000 01101 01011 00001。
最後,用0-9、b-z(去掉a, i, l, o)這32個字母進行base32編碼,得到(39.92324, 116.3906)的編碼爲wx4g0ec1。
解碼算法與編碼算法相反,先進行base32解碼,然後分離出經緯度,最後根據二進制編碼對經緯度範圍進行細分即可,這裏不再贅述。
不過由於geohash表示的是區間,編碼越長越精確,但不可能解碼出完全一致的地址。
引用阿里雲以爲技術專家的博客上的討論:
常見的一些應用場景
A、如果想查詢附近的點?如何操作
查出改點的gehash值,然後到數據庫裏面進行前綴匹配就可以了。
B、如果想查詢附近點,特定範圍內,例如一個點周圍500米的點,如何搞?
可以查詢結果,在結果中進行賽選,將geohash進行解碼爲經緯度,然後進行比較
1、在緯度相等的情況下:
經度每隔0.00001度,距離相差約1米
經度每隔0.0001度,距離相差約10米
經度每隔0.001度,距離相差約100米
經度每隔0.01度,距離相差約1000米
經度每隔0.1度,距離相差約10000米
2、在經度相等的情況下:
緯度每隔0.00001度,距離相差約1.1米
緯度每隔0.0001度,距離相差約11米
緯度每隔0.001度,距離相差約111米
緯度每隔0.01度,距離相差約1113米
緯度每隔0.1度,距離相差約11132米
代碼直接貼出來,感興趣的直接運行一下吧呵呵。
- import java.text.DecimalFormat;
- import java.util.BitSet;
- import java.util.HashMap;
- public class Geohash {
- private static int numbits = 6 * 5;
- private static String data = "y8dcb88bgcqs#KP2#wx4g0ebcgcnw#KP2#wx4g0ec9er26#KP2#wx4g0ec9g30q";
- final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
- '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',
- 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
- final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();
- static {
- int i = 0;
- for (char c : digits)
- lookup.put(c, i++);
- }
- public static void main(String[] args) throws Exception {
- double lon = 116.39036, lat = 39.92324;
- System.out.println(new Geohash().encode(39.92325, 116.39136));
- System.out.println(getFinalResult(data, 100, lon, lat));
- }
- public static double[] decode(String geohash) {
- StringBuilder buffer = new StringBuilder();
- for (char c : geohash.toCharArray()) {
- int i = lookup.get(c) + 32;
- buffer.append(Integer.toString(i, 2).substring(1));
- }
- BitSet lonset = new BitSet();
- BitSet latset = new BitSet();
- // even bits
- int j = 0;
- for (int i = 0; i < numbits * 2; i += 2) {
- boolean isSet = false;
- if (i < buffer.length())
- isSet = buffer.charAt(i) == '1';
- lonset.set(j++, isSet);
- }
- // odd bits
- j = 0;
- for (int i = 1; i < numbits * 2; i += 2) {
- boolean isSet = false;
- if (i < buffer.length())
- isSet = buffer.charAt(i) == '1';
- latset.set(j++, isSet);
- }
- double lon = decode(lonset, -180, 180);
- double lat = decode(latset, -90, 90);
- return new double[] { lon, lat };
- }
- private static double decode(BitSet bs, double floor, double ceiling) {
- double mid = 0;
- for (int i = 0; i < bs.length(); i++) {
- mid = (floor + ceiling) / 2;
- if (bs.get(i))
- floor = mid;
- else
- ceiling = mid;
- }
- return mid;
- }
- public String encode(double lat, double lon) {
- BitSet latbits = getBits(lat, -90, 90);
- BitSet lonbits = getBits(lon, -180, 180);
- StringBuilder buffer = new StringBuilder();
- for (int i = 0; i < numbits; i++) {
- buffer.append((lonbits.get(i)) ? '1' : '0');
- buffer.append((latbits.get(i)) ? '1' : '0');
- }
- return base32(Long.parseLong(buffer.toString(), 2));
- }
- private BitSet getBits(double lat, double floor, double ceiling) {
- BitSet buffer = new BitSet(numbits);
- for (int i = 0; i < numbits; i++) {
- double mid = (floor + ceiling) / 2;
- if (lat >= mid) {
- buffer.set(i);
- floor = mid;
- } else {
- ceiling = mid;
- }
- }
- return buffer;
- }
- public static String base32(long i) {
- char[] buf = new char[65];
- int charPos = 64;
- boolean negative = (i < 0);
- if (!negative)
- i = -i;
- while (i <= -32) {
- buf[charPos--] = digits[(int) (-(i % 32))];
- i /= 32;
- }
- buf[charPos] = digits[(int) (-i)];
- if (negative)
- buf[--charPos] = '-';
- return new String(buf, charPos, (65 - charPos));
- }
- /**
- * 獲取圓內的所有結果
- * @param myData sql中like前4位(wq32%)
- * @param radius 附近距離相當於一個圓的半徑
- * @param longitude 經度
- * @param latitude 緯度
- * @return
- */
- public static String getFinalResult(String myData, int radius,
- double longitude, double latitude) {
- String finalResult = "";
- try {
- if (myData != null && !"".equals(myData)) {
- // 實際經度半徑
- double lonRadius = radius % 1000 == 0 ? (0.01 * radius / 1000)
- : (radius % 100 == 0 ? (0.001 * radius / 100)
- : (radius % 10 == 0 ? (0.0001 * radius / 10)
- : (0.00001 * radius)));
- // 實際緯度半徑
- double latRadius = radius % 1000 == 0 ? (0.01 * radius * radius / (1113 * 1000))
- : (radius % 100 == 0 ? (0.001 * radius * radius / (100 * 111))
- : (radius % 10 == 0 ? (0.0001 * radius * radius / (10 * 11))
- : (0.00001 * radius * radius / 1.1)));
- String[] dataSplit = myData.split("#KP2#");
- for (int i = 0; i < dataSplit.length; i++) {
- String myTemp = dataSplit[i];
- //當前的緯度
- double currentLat = Geohash.decode(myTemp)[1];
- //當前的經度
- double currentLon = Geohash.decode(myTemp)[0];
- //當前的緯度和圓中心的緯度差
- double y = getFiveDecimal(Math.abs(latitude - currentLat)) * radius / latRadius;
- //當前的經度和圓中心的經度差
- double x = getFiveDecimal(Math.abs(longitude - currentLon)) * radius / lonRadius;
- //判斷當前點是否在圓內
- if ((x * x + y * y) <= (radius * radius)) {
- finalResult += myTemp + "#KP2#";
- }
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- finalResult = "";
- }
- if (finalResult != null && !"".equals(finalResult)) {
- finalResult = finalResult.substring(0, finalResult.length() - 5);
- }
- return finalResult;
- }
- // 獲取最多保留5位小數點
- public static double getFiveDecimal(double d) {
- DecimalFormat df = new DecimalFormat("0.00000");
- return Double.parseDouble(df.format(d));
- }
- }