NewGeoCoding:一种外卖场景下的GeoCoding算法

地理编码(GeoCoding)是地图服务中一项重要的功能,它提供了将详细的结构化文本地址转换为经纬度坐标的能力,比如:

  • 北京市朝阳区阜通东大街6号 转换后经纬度:116.480881,39.989410

在外卖场景下,GeoCoding将用户填写的送餐地址文本转换成经纬度,作为骑手送餐的目标经纬度,所以GeoCoding算法准确性的高低直接影响到骑手送达效率。目前订单地址文档到经纬度的映射是通过高德GeoCoding接口实现的,由于高德GeoCoding接口提供的是一种通用的地理编码功能,对外卖场景下订单的识别效果很多情况下不够完美。体现在以下几个方面:

  1. Poi库中小区的楼栋建筑覆盖不全
    高德的使用场景更多是从一个poi导航到另一个poi,而对于一个小区内部的楼栋覆盖率并不是最重要的考量,导致很多小区内部的楼栋缺失。而外卖场景下,用户填写的下单地址很多是小区里面的某个楼栋,这种情况下使用高德GC服务进行经纬度反解效果不够理想。例如:


    “建材城中路27号金隅智造工场N3饿了么”GC效果
  2. 新楼栋发现和更新比较慢
    对于poi库中缺失的小区内部楼栋,高德更新频率比较慢,相比之下只要该楼栋有用户下单,我们就可以发现潜在的缺失楼栋信息,并及时补充到我们的poi库中

  3. 地址匹配算法的效果不够精准
    外卖场景的地址匹配单纯从文本相似度进行计算是不够的,还需要同时加入对多级地址实体(Entity)间相似度的判别,比如下面三个地址文本:
    领秀慧谷13号楼
    领秀慧谷133号楼
    领秀慧谷9号楼
    如果单纯从文本相似度来看,“领秀慧谷13号楼”和“领秀慧谷133号楼”相似度应该高于和“领秀慧谷9号楼”的相似度,但是如果从地址实体的角度,13号楼和9号楼的位置应该更接近,而不是133号楼,这样来看“领秀慧谷13号楼”和“领秀慧谷9号楼”相似度应该更高。

  4. 对两段地址支持不够好
    在饿了么app上用户下单时,采用的是两段地址的格式,也就是用户先选择一个poi,然后自己手填一部分详细地址信息。这两段地址往往是简单的首尾拼接起来,然后进行GeoCoding。如果这两段地址存在互相矛盾,重复,顺序颠倒的情况,高德GC反解的效果也会大打折扣甚至错误。


    两段地址示例

基于以上的现状,我们设计并实现了一套针对外卖场景的地址反解算法,我们称为NGC(NewGeoCoding)。NGC的核心功能是将用户填写的两段地址绑定到poi库中匹配程度最高的一条poi记录上,然后使用该poi的经纬度作为地址反解的经纬度。对该算法有如下的要求:

  1. 能够准确的理解用户地址中对道路、门牌号、小区名称、片区、楼栋、单元、楼层、房间号的表述,从文本相似度+实体相似度两个维度计算相似度
  2. 能够处理两段地址中存在的前后矛盾、重复、部分地址实体缺失、实体位置颠倒等错误,有较高的鲁棒性
  3. 对于绑定失败的地址,能够返回地址中表述的小区+楼栋号,作为新poi挖掘的候选数据
  4. 要能处理重名poi的匹配
  5. 匹配准确度要足够高,同时保持较低的耗时

NGC的整体流程包含三个主要步骤:两段地址合并,获取周边相似poi,地址和周边poi匹配:


NGC流程图
两段地址合并

用户在饿了么下单时地址是两段式地址,理想情况下两段地址应该清晰准确,互为补充,但由于第二段用户手动填写的地址部分是不受限制的,所以用户可以随意填写,导致了一系列的地址问题,包括这两段地址存在互相矛盾,重复,对同一个小区或楼栋前后采用不同的表述,地址中实体成分顺序颠倒混乱等情况。所以首先要将两段地址合并成一段完整、合理、不存在歧义和矛盾、不包含重复内容、条理清晰的地址,见下面的例子。


两段地址合并示例

下文中我们使用/表示两段文本地址,如“饿了么/建材城中路27号”,代表第一段地址是“饿了么”,第二段地址是“建材城中路27号”。

两段地址内容重复是两段地址合并中首先需要处理的问题,内容重复包含几个层面的含义:

  1. 文字层面完全重复,如“安河家园六里/北京市海淀区安河家园六里4号楼”,第二段地址完全包含了第一段地址“安河家园六里”。这是一种比较好解决的问题,通过字符串包含判断就可以简单处理。
  2. 文字层面分段完全重复,如“苏庄一里13号楼/苏庄一里东区13号楼”,第二段地址虽然没有完全连续包含第一段地址,但是如果把第一段地址切分成“苏庄一里”“13号楼”两部分,就分别被第二段地址包含了,这种情况我们称为分段完全包含,也属于重复的情况,这种情况采用分段匹配算法可以解决。
  3. 包含标点和大小写的重复,如“燕保·阜盛家园(N1门) 2号楼/燕保阜盛家园n1门二号楼”,直接进行字符串比较并不包含,但是去掉标点,大写转小写,并将汉字的数字转换成阿拉伯数字后,就可以判断出重复的情况了。
  4. 楼栋重复,如“领秀慧谷38号楼/领秀慧谷38栋”,两段地址的楼栋号后缀不同,但实际表述的是同一个楼栋号,这种情况我们先对楼栋后缀归一化成相同的形式,在进行重复性判断。
  5. 地址别名重复,如“金科城二楼/金科世界城二楼”,这里“金科城二楼”是“金科世界城二楼”的别名,所以它们是等价的。这种情况需要预生成地址别名数据库,将二者对应起来。目前我们采用拼音Tri-Gram方法生成了几十万条地址别名数据可用于别名处理。
  6. 地址实体部分重复,如“惠康嘉园6区/6区2号楼二单元604”,两段地址中都包含了“6区”这个小区片区信息,这种情况我们利用NER技术首先对两段地址进行实体解析,然后对相同类型的实体判断是否重复,做去重处理。我们支持的NER类型如下:


    地址实体识别类型

经过上面的预处理后,我们完成了对重复内容的检测,接下来就是怎么拼接两段地址,我们基于观察总结出两段文本的四种拼接方式:承接式、包含式、描述式和插入式
承接式地址指的是两段地址存在地址实体结构的由高到低的关系,比如“管庄西里24号楼/2单元302”,两段地址从前往后依次是“小区/楼栋号/单元号/房间号”,这种情况只需要把两段地址顺序拼接就可以了。
包含式地址指的是两段地址存在一段包含另一段地址的情况,这种情况直接使用较长地址即可。
描述式地址指的是第一段地址是一个poi,第二段地址是对这个poi位置的描述,比如“巫山烤鱼/昌平区长江街一号院奇点中心3楼”,对于这种地址需要交换两段地址的位置,第二段地址在前,第一段地址在后,组合成“昌平区长江街一号院奇点中心3楼巫山烤鱼”。
插入式地址指的是需要将第一段地址插入第二段地址的某个位置,比如“信达大厦/北京西路1399号3楼”,需要将“信达大厦”插入到“北京西路1399号”和“3楼”中间,构成“北京西路1399号信达大厦3楼”。
这样,我们完成了两段地址合并成一段地址的处理。

获取周边相似poi

获取周边相似poi是基于用户订单的经纬度和地址文本,搜索其周围相似度最高的top N条poi数据,是一个粗筛的过程。拿到这N条记录后,后续再进行更为精确的相似度打分。
我们首先计算当前订单经纬度所在的精度为7的GeoHash网格,连同周边八个邻居网格,共9个网格。将poi数据预处理后存入ElasticSearch,首先过滤出9个GeoHash网格的poi数据,然后利用ES的Match方法获取其中top N的记录,进行后续的poi绑定。如果poi绑定失败了,我们会扩大网格范围到精度为6继续搜索,如果还是绑定失败了继续扩大到整个城市范围,返回最终匹配的结果,或者null。所以搜索的过程是一个逐层扩大的过程,这样可以提高绑定的精度,减少同城同名poi带来的影响。

地址和周边poi相似度计算

现在我们已经有了合并后的订单地址文本,同时拿到了周边相似度最高的N条poi记录,接下来需要计算该地址文本和哪条poi记录绑定最合适。
该过程分两步,第一步是冲订单地址文本中抽取可能的poi名称,对于一个订单会从不同的维度抽取出多个poi名称,比如“朝阳门内大街15号富力小区91号楼2单元401室”,可以抽取出如下的候选poi名称:

朝阳门内大街15号
朝阳门内大街15号富力小区
富力小区91号楼
富力小区91号楼2单元
富力小区

我们拿这些候选poi名称分别和周边N条poi进行匹配,计算出得分最高的一条poi地址作为最终绑定的结果。打分的过程主要基于两方面,一方面看候选poi和周边poi的名称相似度,相似度越高得分越高;另一方面看候选poi本身所包含的实体的丰富程度,细节越丰富,得分越高。

推荐阅读更多精彩内容