经过一系列的沟通下来,可以通过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));
- }
- }