atlus 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atlus/__about__.py +3 -0
- atlus/__init__.py +44 -0
- atlus/atlus.py +519 -0
- atlus/objects.py +47 -0
- atlus/resources.py +522 -0
- atlus-0.2.2.dist-info/METADATA +85 -0
- atlus-0.2.2.dist-info/RECORD +9 -0
- atlus-0.2.2.dist-info/WHEEL +4 -0
- atlus-0.2.2.dist-info/licenses/LICENSE.txt +9 -0
atlus/__about__.py
ADDED
atlus/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""`atlus` is a Python package to convert raw address and phone number strings into the OSM format.
|
|
2
|
+
It's designed to be used with US and Canadian phone numbers and addresses.
|
|
3
|
+
|
|
4
|
+
```python
|
|
5
|
+
>>> import atlus
|
|
6
|
+
>>> atlus.abbrs("St. Francis")
|
|
7
|
+
"Saint Francis"
|
|
8
|
+
>>> atlus.get_address("789 Oak Dr, Smallville California, 98765")[0]
|
|
9
|
+
{"addr:housenumber": "789", "addr:street": "Oak Drive", "addr:city": "Smallville", "addr:state": "CA", "addr:postcode": "98765"}
|
|
10
|
+
>>> atlus.get_phone("(202) 900-9019")
|
|
11
|
+
"+1 202-900-9019"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# SPDX-FileCopyrightText: 2024-present Will <wahubsch@gmail.com>
|
|
17
|
+
#
|
|
18
|
+
# SPDX-License-Identifier: MIT
|
|
19
|
+
|
|
20
|
+
from .atlus import (
|
|
21
|
+
get_address,
|
|
22
|
+
get_phone,
|
|
23
|
+
abbrs,
|
|
24
|
+
get_title,
|
|
25
|
+
mc_replace,
|
|
26
|
+
us_replace,
|
|
27
|
+
ord_replace,
|
|
28
|
+
remove_br_unicode,
|
|
29
|
+
)
|
|
30
|
+
from . import atlus
|
|
31
|
+
from . import resources
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"get_address",
|
|
35
|
+
"get_phone",
|
|
36
|
+
"abbrs",
|
|
37
|
+
"get_title",
|
|
38
|
+
"mc_replace",
|
|
39
|
+
"us_replace",
|
|
40
|
+
"ord_replace",
|
|
41
|
+
"remove_br_unicode",
|
|
42
|
+
"atlus",
|
|
43
|
+
"resources",
|
|
44
|
+
]
|
atlus/atlus.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""Functions and tools to process the raw address strings."""
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from typing import Union, List, Dict, Tuple
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
import usaddress
|
|
7
|
+
import regex
|
|
8
|
+
|
|
9
|
+
from .objects import Address
|
|
10
|
+
from .resources import (
|
|
11
|
+
street_expand,
|
|
12
|
+
direction_expand,
|
|
13
|
+
name_expand,
|
|
14
|
+
state_expand,
|
|
15
|
+
saint_comp,
|
|
16
|
+
abbr_join_comp,
|
|
17
|
+
dir_fill_comp,
|
|
18
|
+
sr_comp,
|
|
19
|
+
usa_comp,
|
|
20
|
+
paren_comp,
|
|
21
|
+
grid_comp,
|
|
22
|
+
post_comp,
|
|
23
|
+
street_comp,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
toss_tags = [
|
|
27
|
+
"Recipient",
|
|
28
|
+
"IntersectionSeparator",
|
|
29
|
+
"LandmarkName",
|
|
30
|
+
"USPSBoxGroupID",
|
|
31
|
+
"USPSBoxGroupType",
|
|
32
|
+
"USPSBoxID",
|
|
33
|
+
"USPSBoxType",
|
|
34
|
+
"OccupancyType",
|
|
35
|
+
]
|
|
36
|
+
"""Tags from the `usaddress` package to remove."""
|
|
37
|
+
|
|
38
|
+
osm_mapping = {
|
|
39
|
+
"AddressNumber": "addr:housenumber",
|
|
40
|
+
"AddressNumberPrefix": "addr:housenumber",
|
|
41
|
+
"AddressNumberSuffix": "addr:housenumber",
|
|
42
|
+
"StreetName": "addr:street",
|
|
43
|
+
"StreetNamePreDirectional": "addr:street",
|
|
44
|
+
"StreetNamePreModifier": "addr:street",
|
|
45
|
+
"StreetNamePreType": "addr:street",
|
|
46
|
+
"StreetNamePostDirectional": "addr:street",
|
|
47
|
+
"StreetNamePostModifier": "addr:street",
|
|
48
|
+
"StreetNamePostType": "addr:street",
|
|
49
|
+
"OccupancyIdentifier": "addr:unit",
|
|
50
|
+
"PlaceName": "addr:city",
|
|
51
|
+
"StateName": "addr:state",
|
|
52
|
+
"ZipCode": "addr:postcode",
|
|
53
|
+
}
|
|
54
|
+
"""Mapping from `usaddress` fields to OSM tags."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_title(value: str, single_word: bool = False) -> str:
|
|
58
|
+
"""Fix ALL-CAPS string.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
>>> get_title("PALM BEACH")
|
|
62
|
+
"Palm Beach"
|
|
63
|
+
>>> get_title("BOSTON")
|
|
64
|
+
"BOSTON"
|
|
65
|
+
>>> get_title("BOSTON", single_word=True)
|
|
66
|
+
"Boston"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
value: String to fix.
|
|
71
|
+
single_word: Whether the string should be fixed even if it is a single word.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
str: Fixed string.
|
|
75
|
+
"""
|
|
76
|
+
if (value.isupper() and " " in value) or (value.isupper() and single_word):
|
|
77
|
+
return mc_replace(value.title())
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def us_replace(value: str) -> str:
|
|
82
|
+
"""Fix string containing improperly formatted US.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
>>> us_replace("U.S. Route 15")
|
|
86
|
+
"US Route 15"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
value: String to fix.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
str: Fixed string.
|
|
94
|
+
"""
|
|
95
|
+
return value.replace("U.S.", "US").replace("U. S.", "US").replace("U S ", "US ")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def mc_replace(value: str) -> str:
|
|
99
|
+
"""Fix string containing improperly formatted Mc- prefix.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
>>> mc_replace("Fort Mchenry")
|
|
103
|
+
"Fort McHenry"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
value: String to fix.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
str: Fixed string.
|
|
111
|
+
"""
|
|
112
|
+
words = []
|
|
113
|
+
for word in value.split():
|
|
114
|
+
mc_match = word.partition("Mc")
|
|
115
|
+
words.append(mc_match[0] + mc_match[1] + mc_match[2].capitalize())
|
|
116
|
+
return " ".join(words)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def ord_replace(value: str) -> str:
|
|
120
|
+
"""Fix string containing improperly capitalized ordinal.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
>>> ord_replace("3Rd St. NW")
|
|
124
|
+
"3rd St. NW"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
value: String to fix.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
str: Fixed string.
|
|
132
|
+
"""
|
|
133
|
+
return regex.sub(r"(\b\d+[SNRT][tTdDhH]\b)", lower_match, value)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def name_street_expand(match: regex.Match) -> str:
|
|
137
|
+
"""Expand matched street type abbreviations.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
match (regex.Match): Matched string.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
str: Expanded string.
|
|
144
|
+
"""
|
|
145
|
+
mat = match.group(1).upper().rstrip(".")
|
|
146
|
+
if mat:
|
|
147
|
+
return ({**name_expand, **street_expand})[mat].title()
|
|
148
|
+
raise ValueError
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def direct_expand(match: regex.Match) -> str:
|
|
152
|
+
"""Expand matched directional abbreviations.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
match (regex.Match): Matched string.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
str: Expanded string.
|
|
159
|
+
"""
|
|
160
|
+
mat = match.group(1).upper().replace(".", "")
|
|
161
|
+
if mat:
|
|
162
|
+
return direction_expand[mat].title()
|
|
163
|
+
raise ValueError
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def cap_match(match: regex.Match) -> str:
|
|
167
|
+
"""Make matches uppercase.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
match (regex.Match): Matched string.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
str: Capitalized string.
|
|
174
|
+
"""
|
|
175
|
+
return "".join(match.groups()).upper().replace(".", "")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def lower_match(match: regex.Match) -> str:
|
|
179
|
+
"""Lower-case improperly cased ordinal values.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
value: String to fix.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
str: Fixed string.
|
|
186
|
+
"""
|
|
187
|
+
return match.group(1).lower()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def grid_match(match_str: regex.Match) -> str:
|
|
191
|
+
"""Clean grid addresses."""
|
|
192
|
+
return match_str.group(0).replace(" ", "").upper()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def abbrs(value: str) -> str:
|
|
196
|
+
"""Bundle most common abbreviation expansion functions.
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
>>> abbrs("St. Francis")
|
|
200
|
+
"Saint Francis"
|
|
201
|
+
>>> abbrs("E St.")
|
|
202
|
+
"E Street"
|
|
203
|
+
>>> abbrs("E Sewell St")
|
|
204
|
+
"East Sewell Street"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
value (str): String to expand.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
str: Expanded string.
|
|
212
|
+
"""
|
|
213
|
+
value = ord_replace(us_replace(mc_replace(get_title(value))))
|
|
214
|
+
|
|
215
|
+
# change likely 'St' to 'Saint'
|
|
216
|
+
value = saint_comp.sub(
|
|
217
|
+
"Saint",
|
|
218
|
+
value,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# expand common street and word abbreviations
|
|
222
|
+
value = abbr_join_comp.sub(
|
|
223
|
+
name_street_expand,
|
|
224
|
+
value,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# expand directionals
|
|
228
|
+
value = dir_fill_comp.sub(
|
|
229
|
+
direct_expand,
|
|
230
|
+
value,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# normalize 'US'
|
|
234
|
+
value = us_replace(value)
|
|
235
|
+
|
|
236
|
+
# uppercase shortened street descriptors
|
|
237
|
+
value = regex.sub(
|
|
238
|
+
r"\b(C[rh]|S[rh]|[FR]m|Us)\b",
|
|
239
|
+
cap_match,
|
|
240
|
+
value,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# remove unremoved abbr periods
|
|
244
|
+
value = regex.sub(
|
|
245
|
+
r"([a-zA-Z]{2,})\.",
|
|
246
|
+
r"\1",
|
|
247
|
+
value,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# expand 'SR' if no other street types
|
|
251
|
+
value = sr_comp.sub("State Route", value)
|
|
252
|
+
return value.strip(" .")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def remove_br_unicode(old: str) -> str:
|
|
256
|
+
"""Clean the input string before sending to parser by removing newlines and unicode.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
old (str): String to clean.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
str: Cleaned string.
|
|
263
|
+
"""
|
|
264
|
+
old = regex.sub(r"<br ?/>", ",", old)
|
|
265
|
+
return regex.sub(r"[^\x00-\x7F\n\r\t]", "", old) # remove unicode
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def clean_address(address_string: str) -> str:
|
|
269
|
+
"""Clean the input string before sending to parser by removing newlines and unicode.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
address_string (str): String to clean.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
str: Cleaned string.
|
|
276
|
+
"""
|
|
277
|
+
address_string = usa_comp.sub(
|
|
278
|
+
"", remove_br_unicode(address_string).replace(" ", " ").strip(" ,.")
|
|
279
|
+
)
|
|
280
|
+
address_string = paren_comp.sub("", address_string)
|
|
281
|
+
return grid_comp.sub(grid_match, address_string)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def help_join(tags, keep: List[str]) -> str:
|
|
285
|
+
"""Help to join address fields."""
|
|
286
|
+
tag_join: List[str] = [v for k, v in tags.items() if k in keep]
|
|
287
|
+
return " ".join(tag_join)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def addr_street(tags: Dict[str, str]) -> str:
|
|
291
|
+
"""Build the street field."""
|
|
292
|
+
return help_join(
|
|
293
|
+
tags,
|
|
294
|
+
[
|
|
295
|
+
"StreetName",
|
|
296
|
+
"StreetNamePreDirectional",
|
|
297
|
+
"StreetNamePreModifier",
|
|
298
|
+
"StreetNamePreType",
|
|
299
|
+
"StreetNamePostDirectional",
|
|
300
|
+
"StreetNamePostModifier",
|
|
301
|
+
"StreetNamePostType",
|
|
302
|
+
],
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def addr_housenumber(tags: Dict[str, str]) -> str:
|
|
307
|
+
"""Build the housenumber field."""
|
|
308
|
+
return help_join(
|
|
309
|
+
tags, ["AddressNumberPrefix", "AddressNumber", "AddressNumberSuffix"]
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _combine_consecutive_tuples(
|
|
314
|
+
tuples_list: List[Tuple[str, str]]
|
|
315
|
+
) -> List[Tuple[str, str]]:
|
|
316
|
+
"""Join adjacent `usaddress` fields."""
|
|
317
|
+
combined_list = []
|
|
318
|
+
current_tag = None
|
|
319
|
+
current_value = None
|
|
320
|
+
|
|
321
|
+
for value, tag in tuples_list:
|
|
322
|
+
if tag != current_tag:
|
|
323
|
+
if current_tag:
|
|
324
|
+
combined_list.append((current_value, current_tag))
|
|
325
|
+
current_value, current_tag = value, tag
|
|
326
|
+
else:
|
|
327
|
+
current_value = " ".join(i for i in [current_value, value] if i)
|
|
328
|
+
|
|
329
|
+
if current_tag:
|
|
330
|
+
combined_list.append((current_value, current_tag))
|
|
331
|
+
|
|
332
|
+
return combined_list
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _manual_join(parsed: List[tuple]) -> Tuple[Dict[str, str], List[Union[str, None]]]:
|
|
336
|
+
"""Remove duplicates and join remaining fields."""
|
|
337
|
+
parsed_clean = [i for i in parsed if i[1] not in toss_tags]
|
|
338
|
+
counts = Counter([i[1] for i in parsed_clean])
|
|
339
|
+
ok_tags = [tag for tag, count in counts.items() if count == 1]
|
|
340
|
+
ok_dict: Dict[str, str] = {i[1]: i[0] for i in parsed_clean if i[1] in ok_tags}
|
|
341
|
+
removed = [osm_mapping.get(field) for field, count in counts.items() if count > 1]
|
|
342
|
+
|
|
343
|
+
new_dict: Dict[str, Union[str, None]] = {}
|
|
344
|
+
if "addr:street" not in removed:
|
|
345
|
+
new_dict["addr:street"] = addr_street(ok_dict)
|
|
346
|
+
if "addr:housenumber" not in removed:
|
|
347
|
+
new_dict["addr:housenumber"] = addr_housenumber(ok_dict)
|
|
348
|
+
if "addr:unit" not in removed:
|
|
349
|
+
new_dict["addr:unit"] = ok_dict.get("OccupancyIdentifier")
|
|
350
|
+
if "addr:city" not in removed:
|
|
351
|
+
new_dict["addr:city"] = ok_dict.get("PlaceName")
|
|
352
|
+
if "addr:state" not in removed:
|
|
353
|
+
new_dict["addr:state"] = ok_dict.get("StateName")
|
|
354
|
+
if "addr:postcode" not in removed:
|
|
355
|
+
new_dict["addr:postcode"] = ok_dict.get("ZipCode")
|
|
356
|
+
|
|
357
|
+
return {k: v for k, v in new_dict.items() if v}, removed
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def collapse_list(seq: list) -> list:
|
|
361
|
+
"""Remove duplicates in list while keeping order.
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
>>> collapse_list(["foo", "bar", "foo"])
|
|
365
|
+
["foo", "bar"]
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
seq (list): The list to collapse.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
list: The collapsed list.
|
|
373
|
+
"""
|
|
374
|
+
seen = set()
|
|
375
|
+
seen_add = seen.add
|
|
376
|
+
return [x for x in seq if not (x in seen or seen_add(x))]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def split_unit(address_string: str) -> Dict[str, str]:
|
|
380
|
+
"""Split unit from address string, if present."""
|
|
381
|
+
address_string = address_string.strip(" ")
|
|
382
|
+
if not any(char.isalpha() for char in address_string):
|
|
383
|
+
return {"addr:housenumber": address_string}
|
|
384
|
+
|
|
385
|
+
add_dict = {}
|
|
386
|
+
number = ""
|
|
387
|
+
for char in address_string:
|
|
388
|
+
if char.isdigit():
|
|
389
|
+
number += char
|
|
390
|
+
else:
|
|
391
|
+
break
|
|
392
|
+
|
|
393
|
+
unit = remove_prefix(address_string, number).lstrip(" -,/")
|
|
394
|
+
if unit:
|
|
395
|
+
add_dict["addr:unit"] = unit
|
|
396
|
+
add_dict["addr:housenumber"] = number
|
|
397
|
+
|
|
398
|
+
return add_dict
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def remove_prefix(text: str, prefix: str) -> str:
|
|
402
|
+
"""Remove prefix from string for Python 3.8."""
|
|
403
|
+
if text.startswith(prefix):
|
|
404
|
+
return text[len(prefix) :]
|
|
405
|
+
return text
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def get_address(
|
|
409
|
+
address_string: str,
|
|
410
|
+
) -> Tuple[Dict[str, Union[str, int]], List[Union[str, None]]]:
|
|
411
|
+
"""Process address strings.
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
>>> get_address("345 MAPLE RD, COUNTRYSIDE, PA 24680-0198")[0]
|
|
415
|
+
{"addr:housenumber": "345", "addr:street": "Maple Road",
|
|
416
|
+
"addr:city": "Countryside", "addr:state": "PA", "addr:postcode": "24680-0198"}
|
|
417
|
+
>>> get_address("777 Strawberry St.")[0]
|
|
418
|
+
{"addr:housenumber": "777", "addr:street": "Strawberry Street"}
|
|
419
|
+
>>> address = get_address("222 NW Pineapple Ave Suite A Unit B")
|
|
420
|
+
>>> address[0]
|
|
421
|
+
{"addr:housenumber": "222", "addr:street": "Northwest Pineapple Avenue"}
|
|
422
|
+
>>> address[1]
|
|
423
|
+
["addr:unit"]
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
address_string (str): The address string to process.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Tuple[Dict[str, Union[str, int]], List[Union[str, None]]]:
|
|
431
|
+
The processed address string and the removed fields.
|
|
432
|
+
"""
|
|
433
|
+
try:
|
|
434
|
+
cleaned = usaddress.tag(clean_address(address_string), tag_mapping=osm_mapping)[
|
|
435
|
+
0
|
|
436
|
+
]
|
|
437
|
+
removed = []
|
|
438
|
+
except usaddress.RepeatedLabelError as err:
|
|
439
|
+
collapsed = collapse_list(
|
|
440
|
+
[(i[0].strip(" .,#"), i[1]) for i in err.parsed_string]
|
|
441
|
+
)
|
|
442
|
+
cleaned, removed = _manual_join(_combine_consecutive_tuples(collapsed))
|
|
443
|
+
|
|
444
|
+
for toss in toss_tags:
|
|
445
|
+
cleaned.pop(toss, None)
|
|
446
|
+
|
|
447
|
+
if "addr:housenumber" in cleaned:
|
|
448
|
+
cleaned = {**cleaned, **split_unit(cleaned["addr:housenumber"])}
|
|
449
|
+
|
|
450
|
+
if "addr:street" in cleaned:
|
|
451
|
+
street = abbrs(cleaned["addr:street"])
|
|
452
|
+
cleaned["addr:street"] = street_comp.sub(
|
|
453
|
+
"Street",
|
|
454
|
+
street,
|
|
455
|
+
).strip(".")
|
|
456
|
+
|
|
457
|
+
if "addr:city" in cleaned:
|
|
458
|
+
cleaned["addr:city"] = abbrs(get_title(cleaned["addr:city"], single_word=True))
|
|
459
|
+
|
|
460
|
+
if "addr:state" in cleaned:
|
|
461
|
+
old = cleaned["addr:state"].replace(".", "")
|
|
462
|
+
if old.upper() in state_expand:
|
|
463
|
+
cleaned["addr:state"] = state_expand[old.upper()]
|
|
464
|
+
elif len(old) == 2 and old.upper() in list(state_expand.values()):
|
|
465
|
+
cleaned["addr:state"] = old.upper()
|
|
466
|
+
|
|
467
|
+
if "addr:unit" in cleaned:
|
|
468
|
+
cleaned["addr:unit"] = cleaned["addr:unit"].removeprefix("Space").strip(" #.")
|
|
469
|
+
|
|
470
|
+
if "addr:postcode" in cleaned:
|
|
471
|
+
# remove extraneous postcode digits
|
|
472
|
+
cleaned["addr:postcode"] = post_comp.sub(
|
|
473
|
+
r"\1", cleaned["addr:postcode"]
|
|
474
|
+
).replace(" ", "-")
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
validated: Address = Address.model_validate(dict(cleaned))
|
|
478
|
+
except ValidationError as err:
|
|
479
|
+
bad_fields: list = [each.get("loc", [])[0] for each in err.errors()]
|
|
480
|
+
cleaned_ret = dict(cleaned)
|
|
481
|
+
for each in bad_fields:
|
|
482
|
+
cleaned_ret.pop(each, None)
|
|
483
|
+
|
|
484
|
+
removed.extend(bad_fields)
|
|
485
|
+
validated: Address = Address.model_validate(cleaned_ret)
|
|
486
|
+
|
|
487
|
+
return validated.model_dump(exclude_none=True, by_alias=True), removed
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def get_phone(phone: str) -> str:
|
|
491
|
+
"""Format phone numbers to the US and Canadian standard format of `+1 XXX-XXX-XXXX`.
|
|
492
|
+
|
|
493
|
+
```python
|
|
494
|
+
>>> get_phone("2029009019")
|
|
495
|
+
"+1 202-900-9019"
|
|
496
|
+
>>> get_phone("(202) 900-9019")
|
|
497
|
+
"+1 202-900-9019"
|
|
498
|
+
>>> get_phone("202-900-901")
|
|
499
|
+
ValueError: Invalid phone number: 202-900-901
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
phone (str): The phone number to format.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
str: The formatted phone number.
|
|
507
|
+
|
|
508
|
+
Raises:
|
|
509
|
+
ValueError: If the phone number is invalid.
|
|
510
|
+
"""
|
|
511
|
+
phone_valid = regex.search(
|
|
512
|
+
r"^\(?(?:\+? ?1?[ -.]*)?(?:\(?(\d{3})\)?[ -.]*)(\d{3})[ -.]*(\d{4})$",
|
|
513
|
+
phone,
|
|
514
|
+
)
|
|
515
|
+
if phone_valid:
|
|
516
|
+
return (
|
|
517
|
+
f"+1 {phone_valid.group(1)}-{phone_valid.group(2)}-{phone_valid.group(3)}"
|
|
518
|
+
)
|
|
519
|
+
raise ValueError(f"Invalid phone number: {phone}")
|
atlus/objects.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Define objects for parsing fields."""
|
|
2
|
+
|
|
3
|
+
from typing import Union, Optional
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Address(BaseModel):
|
|
8
|
+
"""Define address parsing fields."""
|
|
9
|
+
|
|
10
|
+
addr_housenumber: Optional[Union[int, str]] = Field(
|
|
11
|
+
alias="addr:housenumber",
|
|
12
|
+
description="The house number that is included in the address.",
|
|
13
|
+
examples=[200, "1200-29"],
|
|
14
|
+
default=None,
|
|
15
|
+
)
|
|
16
|
+
addr_street: Optional[str] = Field(
|
|
17
|
+
alias="addr:street",
|
|
18
|
+
description="The street that the address is located on.",
|
|
19
|
+
examples=["North Spring Street"],
|
|
20
|
+
default=None,
|
|
21
|
+
)
|
|
22
|
+
addr_unit: Optional[str] = Field(
|
|
23
|
+
alias="addr:unit",
|
|
24
|
+
description="The unit number or letter that is included in the address.",
|
|
25
|
+
examples=["B"],
|
|
26
|
+
default=None,
|
|
27
|
+
)
|
|
28
|
+
addr_city: Optional[str] = Field(
|
|
29
|
+
alias="addr:city",
|
|
30
|
+
description="The city that the address is located in.",
|
|
31
|
+
examples=["Los Angeles"],
|
|
32
|
+
default=None,
|
|
33
|
+
)
|
|
34
|
+
addr_state: Optional[str] = Field(
|
|
35
|
+
alias="addr:state",
|
|
36
|
+
pattern=r"^[A-Z]{2}$",
|
|
37
|
+
description="The state or territory of the address.",
|
|
38
|
+
examples=["CA"],
|
|
39
|
+
default=None,
|
|
40
|
+
)
|
|
41
|
+
addr_postcode: Optional[str] = Field(
|
|
42
|
+
alias="addr:postcode",
|
|
43
|
+
pattern=r"^\d{5}(?:\-\d{4})?$",
|
|
44
|
+
description="The postal code of the address.",
|
|
45
|
+
examples=["90012", "90012-4801"],
|
|
46
|
+
default=None,
|
|
47
|
+
)
|
atlus/resources.py
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""Hold info for the processing script."""
|
|
2
|
+
|
|
3
|
+
import regex
|
|
4
|
+
|
|
5
|
+
direction_expand = {
|
|
6
|
+
"NE": "Northeast",
|
|
7
|
+
"SE": "Southeast",
|
|
8
|
+
"NW": "Northwest",
|
|
9
|
+
"SW": "Southwest",
|
|
10
|
+
"N": "North",
|
|
11
|
+
"E": "East",
|
|
12
|
+
"S": "South",
|
|
13
|
+
"W": "West",
|
|
14
|
+
}
|
|
15
|
+
"""Compass direction abbreviations."""
|
|
16
|
+
|
|
17
|
+
name_expand = {
|
|
18
|
+
"ARPT": "airport",
|
|
19
|
+
"BLDG": "building",
|
|
20
|
+
"CONF": "conference",
|
|
21
|
+
"CONV": "convention",
|
|
22
|
+
"CNTR": "center",
|
|
23
|
+
"CTR": "center",
|
|
24
|
+
"DWTN": "downtown",
|
|
25
|
+
"INTL": "international",
|
|
26
|
+
"FT": "fort",
|
|
27
|
+
"MT": "mount",
|
|
28
|
+
"MTN": "mountain",
|
|
29
|
+
"SHPG": "shopping",
|
|
30
|
+
}
|
|
31
|
+
"""Common name abbreviations."""
|
|
32
|
+
|
|
33
|
+
state_expand = {
|
|
34
|
+
"ALABAMA": "AL",
|
|
35
|
+
"ALA": "AL",
|
|
36
|
+
"ALASKA": "AK",
|
|
37
|
+
"ALAS": "AK",
|
|
38
|
+
"ARIZONA": "AZ",
|
|
39
|
+
"ARIZ": "AZ",
|
|
40
|
+
"ARKANSAS": "AR",
|
|
41
|
+
"ARK": "AR",
|
|
42
|
+
"CALIFORNIA": "CA",
|
|
43
|
+
"CALIF": "CA",
|
|
44
|
+
"CAL": "CA",
|
|
45
|
+
"COLORADO": "CO",
|
|
46
|
+
"COLO": "CO",
|
|
47
|
+
"COL": "CO",
|
|
48
|
+
"CONNECTICUT": "CT",
|
|
49
|
+
"CONN": "CT",
|
|
50
|
+
"DELAWARE": "DE",
|
|
51
|
+
"DEL": "DE",
|
|
52
|
+
"DISTRICT OF COLUMBIA": "DC",
|
|
53
|
+
"FLORIDA": "FL",
|
|
54
|
+
"FLA": "FL",
|
|
55
|
+
"FLOR": "FL",
|
|
56
|
+
"GEORGIA": "GA",
|
|
57
|
+
"GA": "GA",
|
|
58
|
+
"HAWAII": "HI",
|
|
59
|
+
"IDAHO": "ID",
|
|
60
|
+
"IDA": "ID",
|
|
61
|
+
"ILLINOIS": "IL",
|
|
62
|
+
"ILL": "IL",
|
|
63
|
+
"INDIANA": "IN",
|
|
64
|
+
"IND": "IN",
|
|
65
|
+
"IOWA": "IA",
|
|
66
|
+
"KANSAS": "KS",
|
|
67
|
+
"KANS": "KS",
|
|
68
|
+
"KAN": "KS",
|
|
69
|
+
"KENTUCKY": "KY",
|
|
70
|
+
"KEN": "KY",
|
|
71
|
+
"KENT": "KY",
|
|
72
|
+
"LOUISIANA": "LA",
|
|
73
|
+
"MAINE": "ME",
|
|
74
|
+
"MARYLAND": "MD",
|
|
75
|
+
"MASSACHUSETTS": "MA",
|
|
76
|
+
"MASS": "MA",
|
|
77
|
+
"MICHIGAN": "MI",
|
|
78
|
+
"MICH": "MI",
|
|
79
|
+
"MINNESOTA": "MN",
|
|
80
|
+
"MINN": "MN",
|
|
81
|
+
"MISSISSIPPI": "MS",
|
|
82
|
+
"MISS": "MS",
|
|
83
|
+
"MISSOURI": "MO",
|
|
84
|
+
"MONTANA": "MT",
|
|
85
|
+
"MONT": "MT",
|
|
86
|
+
"NEBRASKA": "NE",
|
|
87
|
+
"NEBR": "NE",
|
|
88
|
+
"NEB": "NE",
|
|
89
|
+
"NEVADA": "NV",
|
|
90
|
+
"NEV": "NV",
|
|
91
|
+
"NEW HAMPSHIRE": "NH",
|
|
92
|
+
"NEW JERSEY": "NJ",
|
|
93
|
+
"NEW MEXICO": "NM",
|
|
94
|
+
"N MEX": "NM",
|
|
95
|
+
"NEW M": "NM",
|
|
96
|
+
"NEW YORK": "NY",
|
|
97
|
+
"NORTH CAROLINA": "NC",
|
|
98
|
+
"NORTH DAKOTA": "ND",
|
|
99
|
+
"N DAK": "ND",
|
|
100
|
+
"OHIO": "OH",
|
|
101
|
+
"OKLAHOMA": "OK",
|
|
102
|
+
"OKLA": "OK",
|
|
103
|
+
"OREGON": "OR",
|
|
104
|
+
"OREG": "OR",
|
|
105
|
+
"ORE": "OR",
|
|
106
|
+
"PENNSYLVANIA": "PA",
|
|
107
|
+
"PENN": "PA",
|
|
108
|
+
"RHODE ISLAND": "RI",
|
|
109
|
+
"SOUTH CAROLINA": "SC",
|
|
110
|
+
"SOUTH DAKOTA": "SD",
|
|
111
|
+
"S DAK": "SD",
|
|
112
|
+
"TENNESSEE": "TN",
|
|
113
|
+
"TENN": "TN",
|
|
114
|
+
"TEXAS": "TX",
|
|
115
|
+
"TEX": "TX",
|
|
116
|
+
"UTAH": "UT",
|
|
117
|
+
"VERMONT": "VT",
|
|
118
|
+
"VIRGINIA": "VA",
|
|
119
|
+
"WASHINGTON": "WA",
|
|
120
|
+
"WASH": "WA",
|
|
121
|
+
"WEST VIRGINIA": "WV",
|
|
122
|
+
"W VA": "WV",
|
|
123
|
+
"WISCONSIN": "WI",
|
|
124
|
+
"WIS": "WI",
|
|
125
|
+
"WISC": "WI",
|
|
126
|
+
"WYOMING": "WY",
|
|
127
|
+
"WYO": "WY",
|
|
128
|
+
"ONTARIO": "ON",
|
|
129
|
+
"QUEBEC": "QC",
|
|
130
|
+
"NOVA SCOTIA": "NS",
|
|
131
|
+
"NEW BRUNSWICK": "NB",
|
|
132
|
+
"MANITOBA": "MB",
|
|
133
|
+
"BRITISH COLUMBIA": "BC",
|
|
134
|
+
"PRINCE EDWARD ISLAND": "PE",
|
|
135
|
+
"PRINCE EDWARD": "PE",
|
|
136
|
+
"SASKATCHEWAN": "SK",
|
|
137
|
+
"ALBERTA": "AB",
|
|
138
|
+
"NEWFOUNDLAND AND LABRADOR": "NL",
|
|
139
|
+
"NEWFOUNDLAND & LABRADOR": "NL",
|
|
140
|
+
"NEWFOUNDLAND": "NL",
|
|
141
|
+
"YUKON": "YK",
|
|
142
|
+
"NUNAVUT": "NU",
|
|
143
|
+
"NORTHWEST TERRITORIES": "NT",
|
|
144
|
+
"NW TERRITORIES": "NT",
|
|
145
|
+
}
|
|
146
|
+
"""Map states to abbreviations."""
|
|
147
|
+
|
|
148
|
+
street_expand = {
|
|
149
|
+
"ACC": "ACCESS",
|
|
150
|
+
"ALY": "ALLEY",
|
|
151
|
+
"ANX": "ANEX",
|
|
152
|
+
"ARC": "ARCADE",
|
|
153
|
+
"AV": "AVENUE",
|
|
154
|
+
"AVE": "AVENUE",
|
|
155
|
+
"BYU": "BAYOU",
|
|
156
|
+
"BCH": "BEACH",
|
|
157
|
+
"BND": "BEND",
|
|
158
|
+
"BLF": "BLUFF",
|
|
159
|
+
"BLFS": "BLUFFS",
|
|
160
|
+
"BTM": "BOTTOM",
|
|
161
|
+
"BLVD": "BOULEVARD",
|
|
162
|
+
"BR": "BRANCH",
|
|
163
|
+
"BRG": "BRIDGE",
|
|
164
|
+
"BRK": "BROOK",
|
|
165
|
+
"BRKS": "BROOKS",
|
|
166
|
+
"BG": "BURG",
|
|
167
|
+
"BGS": "BURGS",
|
|
168
|
+
"BYP": "BYPASS",
|
|
169
|
+
"CP": "CAMP",
|
|
170
|
+
"CY": "KEY",
|
|
171
|
+
"CYN": "CANYON",
|
|
172
|
+
"CPE": "CAPE",
|
|
173
|
+
"CTR": "CENTER",
|
|
174
|
+
"CTRS": "CENTERS",
|
|
175
|
+
"CIR": "CIRCLE",
|
|
176
|
+
"CIRS": "CIRCLES",
|
|
177
|
+
"CLF": "CLIFF",
|
|
178
|
+
"CLFS": "CLIFFS",
|
|
179
|
+
"CLB": "CLUB",
|
|
180
|
+
"CMN": "COMMON",
|
|
181
|
+
"CMNS": "COMMONS",
|
|
182
|
+
"COR": "CORNER",
|
|
183
|
+
"CORS": "CORNERS",
|
|
184
|
+
"CRSE": "COURSE",
|
|
185
|
+
"CT": "COURT",
|
|
186
|
+
"CTS": "COURTS",
|
|
187
|
+
"CV": "COVE",
|
|
188
|
+
"CVS": "COVES",
|
|
189
|
+
"CRK": "CREEK",
|
|
190
|
+
"CRES": "CRESCENT",
|
|
191
|
+
"CRST": "CREST",
|
|
192
|
+
"CSWY": "CAUSEWAY",
|
|
193
|
+
"CURV": "CURVE",
|
|
194
|
+
"DL": "DALE",
|
|
195
|
+
"DM": "DAM",
|
|
196
|
+
"DV": "DIVIDE",
|
|
197
|
+
"DR": "DRIVE",
|
|
198
|
+
"DRS": "DRIVES",
|
|
199
|
+
"EST": "ESTATE",
|
|
200
|
+
"EXPY": "EXPRESSWAY",
|
|
201
|
+
"EXPWY": "EXPRESSWAY",
|
|
202
|
+
"EXT": "EXTENSION",
|
|
203
|
+
"EXTS": "EXTENSIONS",
|
|
204
|
+
"FGR": "FORGE",
|
|
205
|
+
"FGRS": "FORGES",
|
|
206
|
+
"FLS": "FALLS",
|
|
207
|
+
"FLD": "FIELD",
|
|
208
|
+
"FLDS": "FIELDS",
|
|
209
|
+
"FLT": "FLAT",
|
|
210
|
+
"FLTS": "FLATS",
|
|
211
|
+
"FRD": "FORD",
|
|
212
|
+
"FRDS": "FORDS",
|
|
213
|
+
"FRST": "FOREST",
|
|
214
|
+
"FRG": "FORGE",
|
|
215
|
+
"FRGS": "FORGES",
|
|
216
|
+
"FRK": "FORK",
|
|
217
|
+
"FRKS": "FORKS",
|
|
218
|
+
"FRY": "FERRY",
|
|
219
|
+
"FRYS": "FERRYS",
|
|
220
|
+
"FOR": "FORD",
|
|
221
|
+
"FORS": "FORDS",
|
|
222
|
+
"FT": "FORT",
|
|
223
|
+
"FWY": "FREEWAY",
|
|
224
|
+
"GD": "GRADE",
|
|
225
|
+
"GDN": "GARDEN",
|
|
226
|
+
"GDNS": "GARDENS",
|
|
227
|
+
"GTWY": "GATEWAY",
|
|
228
|
+
"GLN": "GLEN",
|
|
229
|
+
"GLNS": "GLENS",
|
|
230
|
+
"GN": "GREEN",
|
|
231
|
+
"GNS": "GREENS",
|
|
232
|
+
"GRN": "GREEN",
|
|
233
|
+
"GRNS": "GREENS",
|
|
234
|
+
"GRV": "GROVE",
|
|
235
|
+
"GRVS": "GROVES",
|
|
236
|
+
"HBR": "HARBOR",
|
|
237
|
+
"HBRS": "HARBORS",
|
|
238
|
+
"HGWY": "HIGHWAY",
|
|
239
|
+
"HVN": "HAVEN",
|
|
240
|
+
"HTS": "HEIGHTS",
|
|
241
|
+
"HWY": "HIGHWAY",
|
|
242
|
+
"HL": "HILL",
|
|
243
|
+
"HLS": "HILLS",
|
|
244
|
+
"HOLW": "HOLLOW",
|
|
245
|
+
"INLT": "INLET",
|
|
246
|
+
"IS": "ISLAND",
|
|
247
|
+
"ISS": "ISLANDS",
|
|
248
|
+
"JCT": "JUNCTION",
|
|
249
|
+
"JCTS": "JUNCTIONS",
|
|
250
|
+
"KY": "KEY",
|
|
251
|
+
"KYS": "KEYS",
|
|
252
|
+
"KNL": "KNOLL",
|
|
253
|
+
"KNLS": "KNOLLS",
|
|
254
|
+
"LK": "LAKE",
|
|
255
|
+
"LKS": "LAKES",
|
|
256
|
+
"LNDG": "LANDING",
|
|
257
|
+
"LN": "LANE",
|
|
258
|
+
"LGT": "LIGHT",
|
|
259
|
+
"LGTS": "LIGHTS",
|
|
260
|
+
"LF": "LOAF",
|
|
261
|
+
"LCK": "LOCK",
|
|
262
|
+
"LCKS": "LOCKS",
|
|
263
|
+
"LDG": "LODGE",
|
|
264
|
+
"LP": "LOOP",
|
|
265
|
+
"MNR": "MANOR",
|
|
266
|
+
"MNRS": "MANORS",
|
|
267
|
+
"MDW": "MEADOW",
|
|
268
|
+
"MDWS": "MEADOWS",
|
|
269
|
+
"ML": "MILL",
|
|
270
|
+
"MLS": "MILLS",
|
|
271
|
+
"MSN": "MISSION",
|
|
272
|
+
"MTWY": "MOTORWAY",
|
|
273
|
+
"MT": "MOUNT",
|
|
274
|
+
"MTN": "MOUNTAIN",
|
|
275
|
+
"MTNS": "MOUNTAINS",
|
|
276
|
+
"NCK": "NECK",
|
|
277
|
+
"ORCH": "ORCHARD",
|
|
278
|
+
"OPAS": "OVERPASS",
|
|
279
|
+
"PKY": "PARKWAY",
|
|
280
|
+
"PKWY": "PARKWAY",
|
|
281
|
+
"PSGE": "PASSAGE",
|
|
282
|
+
"PNE": "PINE",
|
|
283
|
+
"PNES": "PINES",
|
|
284
|
+
"PL": "PLACE",
|
|
285
|
+
"PLN": "PLAIN",
|
|
286
|
+
"PLNS": "PLAINS",
|
|
287
|
+
"PLZ": "PLAZA",
|
|
288
|
+
"PT": "POINT",
|
|
289
|
+
"PTS": "POINTS",
|
|
290
|
+
"PRT": "PORT",
|
|
291
|
+
"PRTS": "PORTS",
|
|
292
|
+
"PR": "PRAIRIE",
|
|
293
|
+
"PVT": "PRIVATE",
|
|
294
|
+
"RADL": "RADIAL",
|
|
295
|
+
"RNCH": "RANCH",
|
|
296
|
+
"RPD": "RAPID",
|
|
297
|
+
"RPDS": "RAPIDS",
|
|
298
|
+
"RST": "REST",
|
|
299
|
+
"RDG": "RIDGE",
|
|
300
|
+
"RDGS": "RIDGES",
|
|
301
|
+
"RIV": "RIVER",
|
|
302
|
+
"RD": "ROAD",
|
|
303
|
+
"RDS": "ROADS",
|
|
304
|
+
"RT": "ROUTE",
|
|
305
|
+
"RTE": "ROUTE",
|
|
306
|
+
"SHL": "SHOAL",
|
|
307
|
+
"SHLS": "SHOALS",
|
|
308
|
+
"SHR": "SHORE",
|
|
309
|
+
"SHRS": "SHORES",
|
|
310
|
+
"SKWY": "SKYWAY",
|
|
311
|
+
"SPG": "SPRING",
|
|
312
|
+
"SPGS": "SPRINGS",
|
|
313
|
+
"SQ": "SQUARE",
|
|
314
|
+
"SQS": "SQUARES",
|
|
315
|
+
"STA": "STATION",
|
|
316
|
+
"STRA": "STRAVENUE",
|
|
317
|
+
"STRM": "STREAM",
|
|
318
|
+
"STS": "STREETS",
|
|
319
|
+
"SMT": "SUMMIT",
|
|
320
|
+
"SRVC": "SERVICE",
|
|
321
|
+
"TER": "TERRACE",
|
|
322
|
+
"TRWY": "THROUGHWAY",
|
|
323
|
+
"THFR": "THOROUGHFARE",
|
|
324
|
+
"TRCE": "TRACE",
|
|
325
|
+
"TRAK": "TRACK",
|
|
326
|
+
"TRFY": "TRAFFICWAY",
|
|
327
|
+
"TRL": "TRAIL",
|
|
328
|
+
"TRLR": "TRAILER",
|
|
329
|
+
"TUNL": "TUNNEL",
|
|
330
|
+
"TPKE": "TURNPIKE",
|
|
331
|
+
"UPAS": "UNDERPASS",
|
|
332
|
+
"UN": "UNION",
|
|
333
|
+
"UNP": "UNDERPASS",
|
|
334
|
+
"UNS": "UNIONS",
|
|
335
|
+
"VIA": "VIADUCT",
|
|
336
|
+
"VIAS": "VIADUCTS",
|
|
337
|
+
"VLY": "VALLEY",
|
|
338
|
+
"VLYS": "VALLEYS",
|
|
339
|
+
"VW": "VIEW",
|
|
340
|
+
"VWS": "VIEWS",
|
|
341
|
+
"VLG": "VILLAGE",
|
|
342
|
+
"VL": "VILLE",
|
|
343
|
+
"VIS": "VISTA",
|
|
344
|
+
"WK": "WALK",
|
|
345
|
+
"WKWY": "WALKWAY",
|
|
346
|
+
"WY": "WAY",
|
|
347
|
+
"WL": "WELL",
|
|
348
|
+
"WLS": "WELLS",
|
|
349
|
+
"XING": "CROSSING",
|
|
350
|
+
"XINGS": "CROSSINGS",
|
|
351
|
+
"XRD": "CROSSROAD",
|
|
352
|
+
"XRDS": "CROSSROADS",
|
|
353
|
+
"YU": "BAYOU",
|
|
354
|
+
}
|
|
355
|
+
"""Common street type abbreviations."""
|
|
356
|
+
|
|
357
|
+
saints = [
|
|
358
|
+
"Abigail",
|
|
359
|
+
"Agatha",
|
|
360
|
+
"Agnes",
|
|
361
|
+
"Andrew",
|
|
362
|
+
"Anthony",
|
|
363
|
+
"Augustine",
|
|
364
|
+
"Bernadette",
|
|
365
|
+
"Brigid",
|
|
366
|
+
"Catherine",
|
|
367
|
+
"Charles",
|
|
368
|
+
"Christopher",
|
|
369
|
+
"Clare",
|
|
370
|
+
"Cloud",
|
|
371
|
+
"Dymphna",
|
|
372
|
+
"Elizabeth",
|
|
373
|
+
"Faustina",
|
|
374
|
+
"Felix",
|
|
375
|
+
"Francis",
|
|
376
|
+
"Gabriel,",
|
|
377
|
+
"George",
|
|
378
|
+
"Gerard",
|
|
379
|
+
"James",
|
|
380
|
+
"Joan",
|
|
381
|
+
"John",
|
|
382
|
+
"Joseph",
|
|
383
|
+
"Jude",
|
|
384
|
+
"Kateri",
|
|
385
|
+
"Louis",
|
|
386
|
+
"Lucie",
|
|
387
|
+
"Lucy",
|
|
388
|
+
"Luke",
|
|
389
|
+
"Maria",
|
|
390
|
+
"Mark",
|
|
391
|
+
"Martin",
|
|
392
|
+
"Mary",
|
|
393
|
+
"Maximilian",
|
|
394
|
+
"Michael",
|
|
395
|
+
"Monica",
|
|
396
|
+
"Padre",
|
|
397
|
+
"Patrick",
|
|
398
|
+
"Paul",
|
|
399
|
+
"Peter",
|
|
400
|
+
"Philomena",
|
|
401
|
+
"Raphael",
|
|
402
|
+
"Rita",
|
|
403
|
+
"Rose",
|
|
404
|
+
"Sebastian",
|
|
405
|
+
"Teresa",
|
|
406
|
+
"Therese",
|
|
407
|
+
"Thomas",
|
|
408
|
+
"Valentine",
|
|
409
|
+
"Victor",
|
|
410
|
+
"Vincent",
|
|
411
|
+
]
|
|
412
|
+
"""Most common saint names."""
|
|
413
|
+
|
|
414
|
+
bad_zip_first_3 = [
|
|
415
|
+
"001",
|
|
416
|
+
"002",
|
|
417
|
+
"003",
|
|
418
|
+
"004",
|
|
419
|
+
"213",
|
|
420
|
+
"269",
|
|
421
|
+
"343",
|
|
422
|
+
"345",
|
|
423
|
+
"348",
|
|
424
|
+
"353",
|
|
425
|
+
"419",
|
|
426
|
+
"428",
|
|
427
|
+
"429",
|
|
428
|
+
"517",
|
|
429
|
+
"518",
|
|
430
|
+
"519",
|
|
431
|
+
"529",
|
|
432
|
+
"533",
|
|
433
|
+
"536",
|
|
434
|
+
"552",
|
|
435
|
+
"568",
|
|
436
|
+
"569",
|
|
437
|
+
"578",
|
|
438
|
+
"579",
|
|
439
|
+
"589",
|
|
440
|
+
"621",
|
|
441
|
+
"632",
|
|
442
|
+
"642",
|
|
443
|
+
"643",
|
|
444
|
+
"659",
|
|
445
|
+
"663",
|
|
446
|
+
"682",
|
|
447
|
+
"694",
|
|
448
|
+
"695",
|
|
449
|
+
"696",
|
|
450
|
+
"697",
|
|
451
|
+
"698",
|
|
452
|
+
"699",
|
|
453
|
+
"702",
|
|
454
|
+
"709",
|
|
455
|
+
"715",
|
|
456
|
+
"732",
|
|
457
|
+
"742",
|
|
458
|
+
"817",
|
|
459
|
+
"818",
|
|
460
|
+
"819",
|
|
461
|
+
"839",
|
|
462
|
+
"848",
|
|
463
|
+
"849",
|
|
464
|
+
"851",
|
|
465
|
+
"854",
|
|
466
|
+
"858",
|
|
467
|
+
"861",
|
|
468
|
+
"862",
|
|
469
|
+
"866",
|
|
470
|
+
"867",
|
|
471
|
+
"868",
|
|
472
|
+
"869",
|
|
473
|
+
"876",
|
|
474
|
+
"886",
|
|
475
|
+
"887",
|
|
476
|
+
"888",
|
|
477
|
+
"892",
|
|
478
|
+
"896",
|
|
479
|
+
"899",
|
|
480
|
+
"909",
|
|
481
|
+
"929",
|
|
482
|
+
"987",
|
|
483
|
+
]
|
|
484
|
+
"""Three-digit combinations that don't represent a zip code."""
|
|
485
|
+
|
|
486
|
+
# pre-compile regex for speed
|
|
487
|
+
ABBR_JOIN = "|".join({**name_expand, **street_expand})
|
|
488
|
+
abbr_join_comp = regex.compile(
|
|
489
|
+
rf"(\b(?:{ABBR_JOIN})\b\.?)(?!')",
|
|
490
|
+
flags=regex.IGNORECASE,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
DIR_FILL = "|".join(r"\.?".join(list(abbr)) for abbr in direction_expand)
|
|
494
|
+
dir_fill_comp = regex.compile(
|
|
495
|
+
rf"(?<!(?:^(?:Avenue) |[\.']))(\b(?:{DIR_FILL})\b\.?)(?!(?:\.?[a-zA-Z]| (?:Street|Avenue)))",
|
|
496
|
+
flags=regex.IGNORECASE,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
sr_comp = regex.compile(
|
|
500
|
+
r"(\bS\.?R\b\.?)(?= \d+)",
|
|
501
|
+
flags=regex.IGNORECASE,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
saint_comp = regex.compile(
|
|
505
|
+
rf"^(St\.?)(?= )|(\bSt\.?)(?= (?:{'|'.join(saints)}))",
|
|
506
|
+
flags=regex.IGNORECASE,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
street_comp = regex.compile(
|
|
510
|
+
r"St\.?(?= [NESW]\.?[EW]?\.?)|(?<=\d[thndstr]{2} )St\.?\b|St\.?$"
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
post_comp = regex.compile(r"(\d{5})-?0{4}")
|
|
514
|
+
|
|
515
|
+
usa_comp = regex.compile(r",? (?:USA?|United States(?: of America)?|Canada)\b")
|
|
516
|
+
|
|
517
|
+
paren_comp = regex.compile(r" ?\(.*\)")
|
|
518
|
+
|
|
519
|
+
# match Wisconsin grid-style addresses: N65w25055, W249 N6620, etc.
|
|
520
|
+
grid_comp = regex.compile(
|
|
521
|
+
r"\b([NnSs]\d{2,}\s*[EeWw]\d{2,}|[EeWw]\d{2,}\s*[NnSs]\d{2,})\b"
|
|
522
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: atlus
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Translate raw address strings into the OSM tagging scheme.
|
|
5
|
+
Project-URL: Documentation, https://whubsch.github.io/atlus_py/index.html
|
|
6
|
+
Project-URL: Issues, https://github.com/whubsch/atlus_py/issues
|
|
7
|
+
Project-URL: Source, https://github.com/whubsch/atlus_py
|
|
8
|
+
Author-email: Will <wahubsch@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
|
+
Keywords: address,geocoding,mapping,openstreetmap,osm
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Requires-Dist: regex
|
|
26
|
+
Requires-Dist: usaddress
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Atlus
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+

|
|
33
|
+

|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
This Python project translates raw address strings into the OpenStreetMap (OSM) tagging scheme. The package only supports US (and to some extent Canadian) addresses. You can try out the package without installing it at [the Atlus website](https://atlus.dev).
|
|
37
|
+
|
|
38
|
+
> [!NOTE]
|
|
39
|
+
> Use of this package does not absolve you from following OSM's [import guidelines](https://wiki.openstreetmap.org/wiki/Import/Guidelines).
|
|
40
|
+
|
|
41
|
+
## Table of Contents
|
|
42
|
+
|
|
43
|
+
- [Features](#features)
|
|
44
|
+
- [Usage](#usage)
|
|
45
|
+
- [Docs](#docs)
|
|
46
|
+
- [License](#license)
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- Expand common street and name abbreviations.
|
|
51
|
+
- Parse address parts correctly and reliably.
|
|
52
|
+
- Get rid of address junk that is not needed for OpenStreetMap tagging.
|
|
53
|
+
- Parse US and Canadian phone numbers into the standard format.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
This package is meant to work with GeoJSON files containing raw address data, including those produced by the [All the Places](https://alltheplaces.xyz) project or [Overture maps](https://wiki.openstreetmap.org/wiki/Overture).
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
pip install atlus
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
>>> import atlus
|
|
65
|
+
>>> atlus.abbrs("St. Francis")
|
|
66
|
+
"Saint Francis"
|
|
67
|
+
>>> atlus.get_address("789 Oak Dr, Smallville California, 98765")
|
|
68
|
+
{"addr:housenumber": "789", "addr:street": "Oak Drive", "addr:city": "Smallville", "addr:state": "CA", "addr:postcode": "98765"}
|
|
69
|
+
>>> atlus.get_phone("(202) 900-9019")
|
|
70
|
+
"+1 202-900-9019"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Docs
|
|
74
|
+
|
|
75
|
+
The documentation for our package is available online at our [documentation page](https://whubsch.github.io/atlus_py/index.html). We would greatly appreciate your contributions to help improve the auto-generated docs; please submit any updates or corrections via pull requests.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE.txt) file for details.
|
|
80
|
+
|
|
81
|
+
## See also
|
|
82
|
+
|
|
83
|
+
- [OpenStreetMap](https://www.openstreetmap.org/)
|
|
84
|
+
- [Atlus](https://wiki.openstreetmap.org/wiki/atlus)
|
|
85
|
+
- [All the Places](https://wiki.openstreetmap.org/wiki/All_the_Places)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
atlus/__about__.py,sha256=Fs7wclAtJ2v8-YvoTdAtThsw1F8Mf23FAnfN2XcN59Y,61
|
|
2
|
+
atlus/__init__.py,sha256=NzltrLPGvaoFTi6a4BfgFNjnSQOl-DmtRTf5bBZ4RSQ,1001
|
|
3
|
+
atlus/atlus.py,sha256=-Z0hVi5tY4ckUuKyPqfCWwfTidx3O7f1tTWItxPGZ4k,13887
|
|
4
|
+
atlus/objects.py,sha256=x7OnajWqLjAO2G9soP-Ge4QvTg1OajlfO807bcUf9lQ,1458
|
|
5
|
+
atlus/resources.py,sha256=rr0mfZAAUSjEwIOhHrOricr7FcevGw642YK4HCeihn0,10123
|
|
6
|
+
atlus-0.2.2.dist-info/METADATA,sha256=HQhYcd3zU7MYAMytL4NPnCU95uhFBIZGIKH4gLV1E_s,3407
|
|
7
|
+
atlus-0.2.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
8
|
+
atlus-0.2.2.dist-info/licenses/LICENSE.txt,sha256=b5WOl3a6Gti3d3gAPHP7TybGNScqVDGytjXwwzLIauU,1064
|
|
9
|
+
atlus-0.2.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|