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