Repository: openaddresses/machine
Pull request #494 (2016-12-13 20:26:53)

fixed prefix/postfix regexes to include subaddresses (not units)
This PR adds sub-address (not units) support to the `prefixed_number` and `postfixed_street` regexes.  Some extraction examples:

* `123 3rd Street` yields `123` as number and `3rd Street` as street
* `123a 3rd Street` yields `123a` as number and `3rd Street` as street
* `123-a 3rd Street` yields `123-a` as number and `3rd Street` as street

Additionally, there is an error in `prefixed_number_pattern` regex that was extracting, for example, `3` from `3rd Street` which creates an erroneous number for numberless ordinal street names.  This appears to be a common problem with a lot `regexp` [implementations](https://github.com/openaddresses/openaddresses/blob/master/sources/us/ak/matanuska_susitna_borough.json#L20) that have copy/pasted the common `number` regex of `"^([0-9]+)"`:

```
>>> import re
>>> number_pattern = re.compile("^([0-9]+)")
>>> number_match = number_pattern.search("3rd Street")
>>> ''.join(number_match.groups())
'3'
>>> street_pattern = re.compile("^(?:[0-9]+ )(.*)")
>>> street_match = street_pattern.search("3rd Street")
>>> ''.join(street_match.groups()) if street_match else 'No street match'
'No street match'
```




Patch:
diff --git a/openaddr/conform.py b/openaddr/conform.py
index e54fee9..134dee9 100644
--- a/openaddr/conform.py
+++ b/openaddr/conform.py
@@ -82,8 +82,19 @@ geometry_types = {
     ogr.wkbUnknown: 'Unknown'
     }
 
-prefixed_number_pattern = re.compile("^\s*([0-9]+)\s+", False)
-postfixed_street_pattern = re.compile("^(?:\s*[0-9]+\s+)?(.*)", False)
+# extracts:
+# - '123' from '123 Main St'
+# - '123a' from '123a Main St'
+# - '123-a' from '123-a Main St'
+# - '' from '3rd St' (the 3 belongs to the street, it's not a house number)
+prefixed_number_pattern = re.compile("^\s*(\d+-?(?:[A-Z]|\d+)?\\b)", re.IGNORECASE)
+
+# extracts:
+# - 'Main St' from '123 Main St'
+# - 'Main St' from '123a Main St'
+# - 'Main St' from '123-a Main St'
+# - 'Main St' from 'Main St'
+postfixed_street_pattern = re.compile("^\s*(?:\d+-?(?:[A-Z]|\d+)?\\b)?\s*(.*)", re.IGNORECASE)
 
 def mkdirsp(path):
     try:


Test patch:
diff --git a/openaddr/tests/conform.py b/openaddr/tests/conform.py
index 2b6fe5d..a25c293 100644
--- a/openaddr/tests/conform.py
+++ b/openaddr/tests/conform.py
@@ -371,6 +371,120 @@ class TestConformTransforms (unittest.TestCase):
         d = row_fxn_postfixed_street(c, d, "street")
         self.assertEqual(e, d)
 
+        "Regex prefixed_number and postfixed_number - ordinal street w/house number"
+        c = { "conform": {
+            "number": {
+                "function": "prefixed_number",
+                "field": "ADDRESS"
+            },
+            "street": {
+                "function": "postfixed_street",
+                "field": "ADDRESS"
+            }
+        } }
+        d = { "ADDRESS": "12 3RD ST" }
+        e = copy.deepcopy(d)
+        e.update({ "OA:number": "12", "OA:street": "3RD ST" })
+
+        d = row_fxn_prefixed_number(c, d, "number")
+        d = row_fxn_postfixed_street(c, d, "street")
+        self.assertEqual(e, d)
+
+        "Regex prefixed_number and postfixed_number - ordinal street w/o house number"
+        c = { "conform": {
+            "number": {
+                "function": "prefixed_number",
+                "field": "ADDRESS"
+            },
+            "street": {
+                "function": "postfixed_street",
+                "field": "ADDRESS"
+            }
+        } }
+        d = { "ADDRESS": "3RD ST" }
+        e = copy.deepcopy(d)
+        e.update({ "OA:number": "", "OA:street": "3RD ST" })
+
+        d = row_fxn_prefixed_number(c, d, "number")
+        d = row_fxn_postfixed_street(c, d, "street")
+        self.assertEqual(e, d)
+
+        "Regex prefixed_number and postfixed_number - combined house number and suffix"
+        c = { "conform": {
+            "number": {
+                "function": "prefixed_number",
+                "field": "ADDRESS"
+            },
+            "street": {
+                "function": "postfixed_street",
+                "field": "ADDRESS"
+            }
+        } }
+        d = { "ADDRESS": "123A 3RD ST" }
+        e = copy.deepcopy(d)
+        e.update({ "OA:number": "123A", "OA:street": "3RD ST" })
+
+        d = row_fxn_prefixed_number(c, d, "number")
+        d = row_fxn_postfixed_street(c, d, "street")
+        self.assertEqual(e, d)
+
+        "Regex prefixed_number and postfixed_number - hyphenated house number and suffix"
+        c = { "conform": {
+            "number": {
+                "function": "prefixed_number",
+                "field": "ADDRESS"
+            },
+            "street": {
+                "function": "postfixed_street",
+                "field": "ADDRESS"
+            }
+        } }
+        d = { "ADDRESS": "123-A 3RD ST" }
+        e = copy.deepcopy(d)
+        e.update({ "OA:number": "123-A", "OA:street": "3RD ST" })
+
+        d = row_fxn_prefixed_number(c, d, "number")
+        d = row_fxn_postfixed_street(c, d, "street")
+        self.assertEqual(e, d)
+
+        "Regex prefixed_number and postfixed_number - queens-style house number"
+        c = { "conform": {
+            "number": {
+                "function": "prefixed_number",
+                "field": "ADDRESS"
+            },
+            "street": {
+                "function": "postfixed_street",
+                "field": "ADDRESS"
+            }
+        } }
+        d = { "ADDRESS": "123-45 3RD ST" }
+        e = copy.deepcopy(d)
+        e.update({ "OA:number": "123-45", "OA:street": "3RD ST" })
+
+        d = row_fxn_prefixed_number(c, d, "number")
+        d = row_fxn_postfixed_street(c, d, "street")
+        self.assertEqual(e, d)
+
+        "Regex prefixed_number and postfixed_number - should be case-insenstive"
+        c = { "conform": {
+            "number": {
+                "function": "prefixed_number",
+                "field": "ADDRESS"
+            },
+            "street": {
+                "function": "postfixed_street",
+                "field": "ADDRESS"
+            }
+        } }
+        d = { "ADDRESS": "123-a 3rD St" }
+        e = copy.deepcopy(d)
+        e.update({ "OA:number": "123-a", "OA:street": "3rD St" })
+
+        d = row_fxn_prefixed_number(c, d, "number")
+        d = row_fxn_postfixed_street(c, d, "street")
+        self.assertEqual(e, d)
+
 class TestConformCli (unittest.TestCase):
     "Test the command line interface creates valid output files from test input"
     def setUp(self):