如何解析文本中的自由形式的街道/邮政地址,以及常规的组件

问题描述 投票:116回答:9

我们主要在美国开展业务,并试图通过将所有地址字段组合到单个文本区域来改善用户体验。但是有一些问题:

  • 用户键入的地址可能不正确或采用标准格式
  • 地址必须分为部分(街道,城市,州等)来处理信用卡付款
  • 用户可以输入的不仅仅是他们的地址(比如他们的名字或公司)
  • 谷歌可以做到这一点,但服务条款和查询限制是禁止的,特别是在预算紧张的情况下

显然,这是一个常见的问题:

有没有办法将地址与周围的文本隔离并将其分解成碎片?是否有正则表达式来解析地址?

api parsing street-address
9个回答
256
投票

当我在地址验证公司工作时,我经常看到这个问题。我在这里发布答案,以便让正在搜索相同问题的程序员更容易访问。我在处理数十亿个地址的公司,我们在这个过程中学到了很多东西。

首先,我们需要了解一些关于地址的事情。

Addresses are not regular

这意味着正则表达式已经出局。我已经看到了这一切,从简单的正则表达式,以非常特定的格式匹配地址,到这个:

/\s+(\d{2,5}\s+)(?![a|p]m\b)(([a-zA-Z|\s+]{1,5}){1,2})?([\s|\,|.]+)?(([a-zA-Z|\s+]{1,30}){1,4})(court|ct|street|st|drive|dr|lane|ln|road|rd|blvd)([\s|\,|.|\;]+)?(([a-zA-Z|\s+]{1,30}){1,2})([\s|\,|.]+)?\b(AK|AL|AR|AZ|CA|CO|CT|DC|DE|FL|GA|GU|HI|IA|ID|IL|IN|KS|KY|LA|MA|MD|ME|MI|MN|MO|MS|MT|NC|ND|NE|NH|NJ|NM|NV|NY|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VA|VI|VT|WA|WI|WV|WY)([\s|\,|.]+)?(\s+\d{5})?([\s|\,|.]+)/i

...到this,900+线级文件在飞行中生成超大规模正则表达式以匹配更多。我不推荐这些(例如,here's a fiddle of the above regex, that makes plenty of mistakes)。没有一个简单的神奇公式可以让它发挥作用。理论上和理论上,不可能将地址与正则表达式匹配。

USPS Publication 28记录了许多可能的地址格式,包括所有关键字和变量。最糟糕的是,地址通常含糊不清。单词可能意味着不止一件事(“St”可以是“圣徒”或“街头”)并且有些词我非常肯定他们发明了。 (谁知道“Stravenue”是街道后缀?)

您需要一些真正了解地址的代码,如果该代码确实存在,那么这是一个商业秘密。但是,如果你真的喜欢它,你可能会自己动手。

Addresses come in unexpected shapes and sizes

以下是一些人为的(但是完整的)地址:

1)  102 main street
    Anytown, state

2)  400n 600e #2, 52173

3)  p.o. #104 60203

即使这些也可能有效:

4)  829 LKSDFJlkjsdflkjsdljf Bkpw 12345

5)  205 1105 14 90210

显然,这些都不是标准化的。标点符号和换行符无法保证。这是发生了什么:

  1. 1号已完成,因为它包含街道地址以及城市和州。有了这些信息,就足以识别地址,并且可以将其视为“可交付的”(通过一些标准化)。
  2. 数字2已完成,因为它还包含一个街道地址(带有二级/单元号)和一个5位数的邮政编码,足以识别一个地址。
  3. 3号是完整的邮政信箱格式,因为它包含邮政编码。
  4. 第4号也是完整的,因为the ZIP code is unique,意味着私人实体或公司购买了该地址空间。独特的邮政编码适用于大批量或集中的交付空间。发往邮政编码12345的任何内容都发送给位于纽约斯克内克塔迪的通用电气公司。这个例子不会特别到达任何人,但USPS仍然能够提供它。
  5. 5号也是完整的,信不信由你。只有这些数字,可以在针对所有可能地址的数据库进行解析时发现完整地址。当您将每个数字视为一个组件时,填写缺少的方向,辅助指示符和ZIP + 4代码是微不足道的。这是它的样子,完全扩展和标准化:

205 N 1105 W Apt 14

比佛利山庄CA 90210-5221

Address data is not your own

在向许可供应商提供官方地址数据的大多数国家/地区,地址数据本身属于管理机构。在美国,USPS拥有地址。加拿大邮政,皇家邮政和其他国家也是如此,尽管每个国家执行或定义所有权的方式略有不同。知道这一点很重要,因为它通常禁止对地址数据库进行逆向工程。您必须小心如何获取,存储和使用数据。

谷歌地图是快速地址修复的常见方法,但TOS相当禁止;例如,如果不显示Google地图,则不能使用他们的数据或API,仅用于非商业目的(除非您付费),并且您无法存储数据(临时缓存除外)。说得通。谷歌的数据是世界上最好的数据之一。但是,Google地图不会验证该地址。如果地址不存在,它仍会显示地址将存在的位置(在您自己的街道上尝试;使用您知道不存在的门牌号码)。这有时很有用,但请注意这一点。

Nominatim的usage policy同样受到限制,特别是对于大批量和商业用途,数据主要来自免费资源,所以它没有得到很好的维护(这是开放项目的性质) - 但是,这可能仍然适合你的需要。它得到了一个伟大的社区的支持。

USPS本身有一个API,但it goes down a lot并没有任何保证也没有支持。它也可能很难使用。有些人在没有问题的情况下谨慎使用它。但很容易错过USPS要求您仅使用其API来确认通过它们发送的地址。

People expect addresses to be hard

不幸的是,我们的社会习惯于期望地址变得复杂。关于这一点,互联网上有很多好的用户体验文章,但事实是,如果你有一个包含单个字段的地址表单,那就是用户期望的内容,即使它使得边缘案例地址更难以适应表单的格式是期望的,或者表单可能需要一个不应该的字段。或者用户不知道在哪里放置他们的地址的某个部分。

我现在可以继续讨论结帐表单的糟糕用户体验,但我只是说将地址组合到一个字段中将是一个受欢迎的变化 - 人们将能够输入他们认为合适的地址而不是试图找出你冗长的形式。但是,此更改将是意外的,用户可能会发现它有点不和谐。请注意这一点。

通过将国家字段放在前面的地址之前,可以缓解部分痛苦。当他们首先填写国家/地区字段时,您知道如何使表单显示。也许您有一个很好的方法来处理单字段美国地址,因此如果他们选择美国,您可以将表单缩减为单个字段,否则显示组件字段。只是要考虑的事情!

Now we know why it's hard; what can you do about it?

USPS通过名为CASS™认证的流程向供应商授权,以向客户提供经过验证的地址。这些供应商可以访问每月更新的USPS数据库。他们的软件必须符合严格的标准才能获得认证,并且他们通常不需要同意上述限制条款。

有许多CASS认证的公司可以处理列表或拥有API:Melissa Data,Experian QAS和SmartyStreets等等。

(由于“广告”得到了批评,我此时已经截断了我的答案。您可以找到适合您的解决方案。)

真相:真的,伙计们,我不会在这些公司工作。这不是广告。


17
投票

libpostal:一个开源库,用于解析地址,使用OpenStreetMap,OpenAddresses和OpenCage中的数据进行训练。

https://github.com/openvenues/libpostalmore info about it

其他工具/服务:


12
投票

有许多街道地址解析器。它们有两种基本风格 - 具有地名和街道名称数据库,以及没有地名和数据库的数据库。

正则表达式街道地址解析器可以获得高达约95%的成功率,而不会有太多麻烦。然后你开始遇到不寻常的情况。 CPAN中的Perl,“Geo :: StreetAddress :: US”,就是那么好。这里有Python和Javascript端口,都是开源的。我在Python中有一个改进的版本,它通过处理更多的案例略微提高了成功率。但是,要获得最后3%的权利,您需要数据库来帮助消除歧义。

具有3位邮政编码和美国州名称和缩写的数据库是一个很大的帮助。当解析器看到一致的邮政编码和状态名称时,它可以开始锁定格式。这对美国和英国非常有效。

正确的街道地址解析从最后开始并向后工作。这就是USPS系统如何做到这一点。地址最不模糊,国家名称,城市名称和邮政编码相对容易识别。街道名称通常可以被隔离。街道上的位置是解析最复杂的地方;在那里你会遇到诸如“五楼”和“Staples Pavillion”之类的东西。那时数据库是一个很大的帮助。


8
投票

更新:Geocode.xyz现在在全球范围内运作。有关示例,请参阅https://geocode.xyz

对于美国,墨西哥和加拿大,请参阅geocoder.ca

例如:

输入:something going on near the intersection of main and arthur kill rd new york

输出:

<geodata>
  <latt>40.5123510000</latt>
  <longt>-74.2500500000</longt>
  <AreaCode>347,718</AreaCode>
  <TimeZone>America/New_York</TimeZone>
  <standard>
    <street1>main</street1>
    <street2>arthur kill</street2>
    <stnumber/>
    <staddress/>
    <city>STATEN ISLAND</city>
    <prov>NY</prov>
    <postal>11385</postal>
    <confidence>0.9</confidence>
  </standard>
</geodata>

您还可以在Web界面中检查结果或以Json或Jsonp的形式输出结果。例如。 I'm looking for restaurants around 123 Main Street, New York


3
投票

没有代码?耻辱!

这是一个简单的JavaScript地址解析器。对于Matt在上面的论文中提出的每一个原因(我几乎100%同意这一点,非常糟糕:地址是复杂类型,人类犯错误;更好地外包和自动化 - 当你能负担得起时)。

但我没有哭,而是决定尝试:

此代码适用于解析findAddressCandidate的大多数Esri结果,也适用于返回单行地址的其他(反向)地理编码器,其中street / city / state用逗号分隔。如果需要或可以编写特定于国家/地区的解析器,则可以扩展。或者只是将其用作案例研究,了解这项练习的挑战性或者我在JavaScript方面的糟糕程度。我承认我只花了大约30分钟(未来的迭代可以添加缓存,zip验证,状态查找以及用户位置上下文),但它适用于我的用例:最终用户看到将地理编码搜索响应解析为4的表单文本框。如果地址解析出错了(除非源数据很差,这很少见),这没什么大不了的 - 用户可以验证并修复它! (但是对于自动化解决方案,可以丢弃/忽略或标记为错误,因此dev可以支持新格式或修复源数据。)

/* 
address assumptions:
- US addresses only (probably want separate parser for different countries)
- No country code expected.
- if last token is a number it is probably a postal code
-- 5 digit number means more likely
- if last token is a hyphenated string it might be a postal code
-- if both sides are numeric, and in form #####-#### it is more likely
- if city is supplied, state will also be supplied (city names not unique)
- zip/postal code may be omitted even if has city & state
- state may be two-char code or may be full state name.
- commas: 
-- last comma is usually city/state separator
-- second-to-last comma is possibly street/city separator
-- other commas are building-specific stuff that I don't care about right now.
- token count:
-- because units, street names, and city names may contain spaces token count highly variable.
-- simplest address has at least two tokens: 714 OAK
-- common simple address has at least four tokens: 714 S OAK ST
-- common full (mailing) address has at least 5-7:
--- 714 OAK, RUMTOWN, VA 59201
--- 714 S OAK ST, RUMTOWN, VA 59201
-- complex address may have a dozen or more:
--- MAGICICIAN SUPPLY, LLC, UNIT 213A, MAGIC TOWN MALL, 13 MAGIC CIRCLE DRIVE, LAND OF MAGIC, MA 73122-3412
*/

var rawtext = $("textarea").val();
var rawlist = rawtext.split("\n");

function ParseAddressEsri(singleLineaddressString) {
  var address = {
    street: "",
    city: "",
    state: "",
    postalCode: ""
  };

  // tokenize by space (retain commas in tokens)
  var tokens = singleLineaddressString.split(/[\s]+/);
  var tokenCount = tokens.length;
  var lastToken = tokens.pop();
  if (
    // if numeric assume postal code (ignore length, for now)
    !isNaN(lastToken) ||
    // if hyphenated assume long zip code, ignore whether numeric, for now
    lastToken.split("-").length - 1 === 1) {
    address.postalCode = lastToken;
    lastToken = tokens.pop();
  }

  if (lastToken && isNaN(lastToken)) {
    if (address.postalCode.length && lastToken.length === 2) {
      // assume state/province code ONLY if had postal code
      // otherwise it could be a simple address like "714 S OAK ST"
      // where "ST" for "street" looks like two-letter state code
      // possibly this could be resolved with registry of known state codes, but meh. (and may collide anyway)
      address.state = lastToken;
      lastToken = tokens.pop();
    }
    if (address.state.length === 0) {
      // check for special case: might have State name instead of State Code.
      var stateNameParts = [lastToken.endsWith(",") ? lastToken.substring(0, lastToken.length - 1) : lastToken];

      // check remaining tokens from right-to-left for the first comma
      while (2 + 2 != 5) {
        lastToken = tokens.pop();
        if (!lastToken) break;
        else if (lastToken.endsWith(",")) {
          // found separator, ignore stuff on left side
          tokens.push(lastToken); // put it back
          break;
        } else {
          stateNameParts.unshift(lastToken);
        }
      }
      address.state = stateNameParts.join(' ');
      lastToken = tokens.pop();
    }
  }

  if (lastToken) {
    // here is where it gets trickier:
    if (address.state.length) {
      // if there is a state, then assume there is also a city and street.
      // PROBLEM: city may be multiple words (spaces)
      // but we can pretty safely assume next-from-last token is at least PART of the city name
      // most cities are single-name. It would be very helpful if we knew more context, like
      // the name of the city user is in. But ignore that for now.
      // ideally would have zip code service or lookup to give city name for the zip code.
      var cityNameParts = [lastToken.endsWith(",") ? lastToken.substring(0, lastToken.length - 1) : lastToken];

      // assumption / RULE: street and city must have comma delimiter
      // addresses that do not follow this rule will be wrong only if city has space
      // but don't care because Esri formats put comma before City
      var streetNameParts = [];

      // check remaining tokens from right-to-left for the first comma
      while (2 + 2 != 5) {
        lastToken = tokens.pop();
        if (!lastToken) break;
        else if (lastToken.endsWith(",")) {
          // found end of street address (may include building, etc. - don't care right now)
          // add token back to end, but remove trailing comma (it did its job)
          tokens.push(lastToken.endsWith(",") ? lastToken.substring(0, lastToken.length - 1) : lastToken);
          streetNameParts = tokens;
          break;
        } else {
          cityNameParts.unshift(lastToken);
        }
      }
      address.city = cityNameParts.join(' ');
      address.street = streetNameParts.join(' ');
    } else {
      // if there is NO state, then assume there is NO city also, just street! (easy)
      // reasoning: city names are not very original (Portland, OR and Portland, ME) so if user wants city they need to store state also (but if you are only ever in Portlan, OR, you don't care about city/state)
      // put last token back in list, then rejoin on space
      tokens.push(lastToken);
      address.street = tokens.join(' ');
    }
  }
  // when parsing right-to-left hard to know if street only vs street + city/state
  // hack fix for now is to shift stuff around.
  // assumption/requirement: will always have at least street part; you will never just get "city, state"  
  // could possibly tweak this with options or more intelligent parsing&sniffing
  if (!address.city && address.state) {
    address.city = address.state;
    address.state = '';
  }
  if (!address.street) {
    address.street = address.city;
    address.city = '';
  }

  return address;
}

// get list of objects with discrete address properties
var addresses = rawlist
  .filter(function(o) {
    return o.length > 0
  })
  .map(ParseAddressEsri);
$("#output").text(JSON.stringify(addresses));
console.log(addresses);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea>
27488 Stanford Ave, Bowden, North Dakota
380 New York St, Redlands, CA 92373
13212 E SPRAGUE AVE, FAIR VALLEY, MD 99201
1005 N Gravenstein Highway, Sebastopol CA 95472
A. P. Croll &amp; Son 2299 Lewes-Georgetown Hwy, Georgetown, DE 19947
11522 Shawnee Road, Greenwood, DE 19950
144 Kings Highway, S.W. Dover, DE 19901
Intergrated Const. Services 2 Penns Way Suite 405, New Castle, DE 19720
Humes Realty 33 Bridle Ridge Court, Lewes, DE 19958
Nichols Excavation 2742 Pulaski Hwy, Newark, DE 19711
2284 Bryn Zion Road, Smyrna, DE 19904
VEI Dover Crossroads, LLC 1500 Serpentine Road, Suite 100 Baltimore MD 21
580 North Dupont Highway, Dover, DE 19901
P.O. Box 778, Dover, DE 19903
714 S OAK ST
714 S OAK ST, RUM TOWN, VA, 99201
3142 E SPRAGUE AVE, WHISKEY VALLEY, WA 99281
27488 Stanford Ave, Bowden, North Dakota
380 New York St, Redlands, CA 92373
</textarea>
<div id="output">
</div>

1
投票

如果你想依赖OSM数据,libpostal非常强大,并且处理许多最常见的地址输入警告。


1
投票

对于美国地址解析,

我更喜欢使用uspdress包,只有pip可用于usaddress

python3 -m pip install usaddress

Documentation PyPi

这对我来说非常有用。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# address_parser.py
import sys
from usaddress import tag
from json import dumps, loads

if __name__ == '__main__':
    tag_mapping = {
        'Recipient': 'recipient',
        'AddressNumber': 'addressStreet',
        'AddressNumberPrefix': 'addressStreet',
        'AddressNumberSuffix': 'addressStreet',
        'StreetName': 'addressStreet',
        'StreetNamePreDirectional': 'addressStreet',
        'StreetNamePreModifier': 'addressStreet',
        'StreetNamePreType': 'addressStreet',
        'StreetNamePostDirectional': 'addressStreet',
        'StreetNamePostModifier': 'addressStreet',
        'StreetNamePostType': 'addressStreet',
        'CornerOf': 'addressStreet',
        'IntersectionSeparator': 'addressStreet',
        'LandmarkName': 'addressStreet',
        'USPSBoxGroupID': 'addressStreet',
        'USPSBoxGroupType': 'addressStreet',
        'USPSBoxID': 'addressStreet',
        'USPSBoxType': 'addressStreet',
        'BuildingName': 'addressStreet',
        'OccupancyType': 'addressStreet',
        'OccupancyIdentifier': 'addressStreet',
        'SubaddressIdentifier': 'addressStreet',
        'SubaddressType': 'addressStreet',
        'PlaceName': 'addressCity',
        'StateName': 'addressState',
        'ZipCode': 'addressPostalCode',
    }
    try:
        address, _ = tag(' '.join(sys.argv[1:]), tag_mapping=tag_mapping)
    except:
        with open('failed_address.txt', 'a') as fp:
            fp.write(sys.argv[1] + '\n')
        print(dumps({}))
    else:
        print(dumps(dict(address)))

运行address_parser.py

 python3 address_parser.py 9757 East Arcadia Ave. Saugus MA 01906
 {"addressStreet": "9757 East Arcadia Ave.", "addressCity": "Saugus", "addressState": "MA", "addressPostalCode": "01906"}

0
投票

在我们的一个项目中,我们使用了以下地址解析器。它以高精度解析了世界上大多数国家的地址。

http://address-parser.net/

它可以作为独立库或作为实时API使用。


0
投票

美国地址的另一个选择是YAddress(由我工作的公司制作)。

这个问题的许多答案都建议使用地理编码工具作为解决方案。重要的是不要混淆地址解析和地理编码;他们不一样。虽然地理编码器可能会将地址分解为组件作为附带好处,但它们通常依赖于非标准地址集。这意味着地理编码器解析的地址可能与官方地址不同。例如,Google地理编码API在曼哈顿称为“第6大道”,USPS称之为“美洲大道”。

© www.soinside.com 2019 - 2024. All rights reserved.