Packages

Library for parsing US Addresses into their individual parts.

Current section

Files

Jump to
address_us lib address_us.ex
Raw

lib/address_us.ex

defmodule Address do
@moduledoc """
Container for the struct that contains the Address information.
"""
@doc """
Struct containing Address information.
"""
defstruct city: nil, plus_4: nil, street: nil, state: nil, postal: nil
end
defmodule Street do
@moduledoc """
Container for the struct that contains the Street information for an address.
"""
@doc """
Struct containing the Street information.
"""
defstruct name: nil, pmb: nil, pre_direction: nil, primary_number: nil,
post_direction: nil, secondary_designator: nil, secondary_value: nil,
suffix: nil
end
defmodule AddressUS.Parser do
@moduledoc """
Parses US Addresses.
"""
@doc """
Parses a raw address into all of its requisite parts according to USPS
suggestions for address parsing.
## Known Bugs
1) if street suffix is left off while parsing a full multi-line address,
it will fail unless there is a comma or newline separating the street
name from the city.
## Examples
iex> AddressUS.Parser.parse_address("2345 S B Street, Denver, CO 80219")
%Address{city: "Denver", plus_4: nil, postal: "80219",
state: "CO", street: %Street{name: "B", pmb: nil,
post_direction: nil, pre_direction: "S", primary_number: "2345",
secondary_designator: nil, secondary_value: nil, suffix: "St"}}
"""
def parse_address(messy_address) when not is_binary(messy_address), do: nil
def parse_address(messy_address) do
address = standardize_address(messy_address)
{postal, plus_4, address_no_postal} = get_postal(address)
{state, address_no_state} = get_state(address_no_postal)
{city, address_no_city} = get_city(address_no_state)
street = parse_address_list(address_no_city)
%Address{postal: postal, plus_4: plus_4, state: state,
city: city, street: street}
end
@doc """
Parses the raw street portion of an address into its requisite parts
according to USPS suggestions for address parsing.
## Examples
iex> AddressUS.Parser.parse_address_line("2345 S. Beade St")
%Street{name: "Beade", pmb: nil, post_direction: nil, pre_direction: "S",
primary_number: "2345", secondary_designator: nil, secondary_value: nil,
suffix: "St"}
"""
def parse_address_line(invalid) when not is_binary(invalid), do: nil
def parse_address_line(messy_address) do
messy_address
|> standardize_address
|> String.split(" ")
|> Enum.reverse
|> parse_address_list
end
@doc """
Removes non-numeric characters from the phone number and then returns the
integer.
## Examples
iex> AddressUS.Parser.clean_phone_number("(303) 310-7802")
3033107802
"""
def clean_phone_number(nil), do: nil
def clean_phone_number(phone) do
{phone_integer, _} = phone
|> safe_replace(~r/\s+/, "")
|> safe_replace("+1", "")
|> safe_replace("-", "")
|> safe_replace("(", "")
|> safe_replace(")", "")
|> Integer.parse
phone_integer
end
@doc """
Removes country code and associated punctuation from the phone number.
## Examples
iex> AddressUS.Parser.filter_country_code("+1 303-310-7802")
"303-310-7802"
iex> AddressUS.Parser.filter_country_code("+1 (303) 310-7802")
"(303) 310-7802"
iex> AddressUS.Parser.filter_country_code("+1-303-310-7802")
"303-310-7802"
iex> AddressUS.Parser.filter_country_code("1-303-310-7802")
"303-310-7802"
"""
def filter_country_code(nil), do: nil
def filter_country_code(phone) do
phone
|> safe_replace(~r/^1\s+|^1-|^\+1\s+|^\+1-/, "")
|> safe_replace(~r/^1\(|^1\s+\(/, "(")
|> safe_replace(~r/\)(\d)/, ") \\1")
end
@doc """
Abbreviates the state provided.
## Example
iex> AddressUS.Parser.abbreviate_state("Wyoming")
"WY"
iex> AddressUS.Parser.abbreviate_state("wyoming")
"WY"
iex> AddressUS.Parser.abbreviate_state("Wyomin")
"Wyomin"
iex> AddressUS.Parser.abbreviate_state(nil)
nil
"""
def abbreviate_state(nil), do: nil
def abbreviate_state(raw_state) do
state = title_case(raw_state)
states = AddressUSConfig.states()
cond do
safe_has_key?(states, state) == true ->
Map.get(states, state)
Enum.member?(Map.values(states), safe_upcase(state)) == true ->
safe_upcase(state)
true ->
state
end
end
@doc """
Converts the country to the 2 digit ISO country code. "US" is default.
## Example
iex> AddressUS.Parser.get_country_code(nil)
"US"
iex> AddressUS.Parser.get_country_code("Afghanistan")
"AF"
iex> AddressUS.Parser.get_country_code("AF")
"AF"
"""
def get_country_code(nil), do: "US"
def get_country_code(country_name) do
codes = AddressUSConfig.countries()
country = safe_upcase(country_name)
case Enum.member?(Map.values(codes), country) do
true -> country
false -> Map.get(codes, country, country_name)
end
end
@doc """
Parses a csv, but instead of parsing at every comma, it only splits at the
last one found. This allows it to handle situations where the first value
parsed has a comma in it that is not part of what you want to parse.
## Example
iex> AddressUS.Parser.parse_csv("test/test.csv")
%{"Something Horrible, (The worst place other than Wyoming)" => "SH",
"Wyoming" => "WY"}
"""
def parse_csv(nil), do: %{}
def parse_csv(csv) do
String.split(File.read!(csv), ~r{\n|\r|\r\n|\n\r})
|> Stream.map(&(String.reverse(&1)))
|> Stream.map(&(String.split(&1, ",", parts: 2)))
|> Stream.map(&(Enum.reverse(&1)))
|> Stream.map(fn(word) -> Enum.map(word, &(String.reverse(&1))) end)
|> Stream.map(&(List.to_tuple(&1)))
|> Stream.filter(&(tuple_size(&1) == 2))
|> Enum.to_list
|> Enum.into(%{})
end
############################################################################
## Parser Functions
############################################################################
# Parses the city name out of the address list and returns
# {city, leftover_address_list}
defp get_city(address) when not is_list(address), do: {nil, nil}
defp get_city([]), do: {nil, nil}
defp get_city(address), do: get_city(address, address, nil, false)
defp get_city([], backup, _city, false), do: {nil, backup}
defp get_city(address, _backup, city, true) do
{safe_replace(city, ",", ""), address}
end
defp get_city(address, backup, city, false) do
[head|tail] = address
tail_head = case length(tail) > 0 do
false -> ""
true -> hd(tail)
end
cond do
is_keyword?(head) && city == nil ->
get_city(tail, backup, merge_names(city, head), false)
String.ends_with?(tail_head, ",") ->
get_city(tail, backup, merge_names(city, head), true)
head |> safe_starts_with?("#") ->
get_city(address, backup, city, true)
Enum.count(clean_hyphenated_street(head)) > 1 ->
get_city(address, backup, city, true)
city != nil && !is_keyword?(head) && address != []
&& is_possible_suite_number?(tail_head) ->
get_city(address, backup, city, true)
city != nil && !is_keyword?(head) && address != [] ->
get_city(tail, backup, merge_names(city, head), false)
city != nil && is_keyword?(head) ->
pre_keyword_white_list = ["SALT", "WEST", "PALM"]
cond do
Enum.member?(pre_keyword_white_list, safe_upcase(tail_head)) ->
get_city(tail, backup, merge_names(city, head), false)
true ->
get_city(address, backup, city, true)
end
is_keyword?(head) ->
get_city(address, backup, city, true)
contains_po_box?(tail) ->
get_city(tail, backup, head, true)
tail == [] ->
get_city(address, backup, city, true)
get_direction_abbreviation(head) != nil ->
get_city(tail, backup, merge_names(city, head), false)
true ->
get_city(tail, backup, merge_names(city, head), false)
end
end
# Parses the number out of the address list and returns
# {number, box, possible_secondary_value, possible_secondary_designator}
defp get_number(address) when not is_list(address) do
{nil, nil, nil, nil, nil}
end
defp get_number([]), do: {nil, nil, nil. nil, nil}
defp get_number(address) do
get_number(address, address, nil, nil, nil, nil, false)
end
defp get_number(address, _backup, number, box, p_val, p_des, true) do
n = if number == "", do: nil, else: number
b = if box == "", do: nil, else: box
v = if p_val == "", do: nil, else: p_val
d = if p_des == "", do: nil, else: p_des
{n, b, v, d, address}
end
defp get_number([], backup, _, _, _, _, false) do
{nil, nil, nil, nil, backup}
end
defp get_number(address, backup, number, box, p_val, p_des, false) do
[head|tail] = address
{tail_head, tail_tail} = case length(tail) do
0 -> {"", []}
1 -> {hd(tail), []}
_ -> {hd(tail), tl(tail)}
end
next_is_number = if length(tail) == 0 do
false
else
string_is_number_or_fraction?(hd(tail))
end
regex = ~r/(\d+)[-\s]*([A-Za-z]+.*)/
cond do
address == [] ->
get_number(backup, backup, number, box, p_val, p_des, true)
contains_po_box?(address) ->
number = address
|> Enum.join(" ")
|> String.split(~r/(?i)BOX\s/)
|> tl
|> hd
get_number([], backup, safe_replace(number, "#", ""), "PO BOX", p_val,
p_des, true)
number == nil && string_is_number_or_fraction?(head) && next_is_number ->
get_number(tl(tail), backup, head <> " " <> hd(tail), box, p_val,
p_des, true)
Enum.member?(address, "&") ->
new_address = address
|> Enum.join(" ")
|> String.split("&")
|> tl
|> hd
|> String.split(" ")
get_number(new_address, backup, nil, box, p_val, p_des, false)
number == nil && string_is_number_or_fraction?(head) ->
alphanumeric = "ABCDFHIJLKMOPQRGTUVXYZ1234567890"
case safe_contains?(alphanumeric, safe_upcase(tail_head)) do
false ->
get_number(tail, backup, head, box, p_val, p_des, true)
true ->
get_number(tail_tail, backup, head, box, safe_upcase(tail_head),
p_des, true)
end
number == nil && string_is_number_or_fraction?(
safe_replace(head, regex, "\\1")) ->
endings = ["ST", "ND", "RD", "TH"]
new_number = safe_replace(head, regex, "\\1")
new_value = safe_replace(head, regex, "\\2")
case Enum.member?(endings, new_value) do
false ->
get_number(tail, backup, new_number, box, new_value, p_des, true)
true ->
get_number(backup, backup, number, box, new_value, p_des, true)
end
number == nil && is_state?(head) ->
get_number(address, backup, number, box, p_val, p_des, true)
safe_contains?(head, "-") ->
[h|t] = String.split("-")
secondary_value = case length(t) do
0 -> nil
_ -> hd(tail)
end
get_number(tail, backup, h, box, secondary_value, "Ste", true)
true ->
get_number(tail, backup, number, box, p_val, p_des, false)
end
end
# Parses the post direction field out of the address list and returns
# {post_direction, leftover_address_list}.
defp get_post_direction(address) when not is_list(address), do: {nil, nil}
defp get_post_direction([]), do: {nil, nil}
defp get_post_direction(address), do: get_post_direction(address, nil, false)
defp get_post_direction(address, post_direction, true) do
{post_direction, address}
end
defp get_post_direction(address, post_direction, false) do
[head|tail] = address
direction_value = get_direction_value(head)
new_direction = case post_direction == nil do
true -> direction_value
false -> direction_value <> post_direction
end
cond do
get_direction_value(head) == "" ->
get_post_direction(address, post_direction, true)
address == [] ->
get_post_direction(address, new_direction, true)
true ->
get_post_direction(tail, new_direction, false)
end
end
# Gets the postal code from an address and returns
# {zip, zip_plus_4, leftover_address_list}.
defp get_postal(address) when not is_binary(address), do: {nil, nil, nil}
defp get_postal(address) do
reversed_address = Enum.reverse(String.split(address, " "))
[possible_postal|leftover_address] = reversed_address
{postal, plus_4} = parse_postal(possible_postal)
case postal do
nil -> {nil, nil, reversed_address}
_ -> {postal, plus_4, leftover_address}
end
end
# Parses the pre direction field out of the address list and returns
# {pre_direction, leftover_address_list}.
defp get_pre_direction(address) when not is_list(address), do: {nil, nil}
defp get_pre_direction([]), do: {nil, nil}
defp get_pre_direction(address), do: get_pre_direction(address, nil, false)
defp get_pre_direction(address, _pre_direction, false) do
[head|tail] = address
{tail_head, tail_tail} = case length(tail) do
0 -> {"", []}
1 -> {hd(tail), []}
_ -> {hd(tail), tl(tail)}
end
tail_tail_head = if length(tail_tail) > 0, do: hd(tail_tail), else: nil
single_word_direction = get_direction_value(head)
next_is_direction = get_direction_value(tail_head) != ""
double_word_direction = get_direction_value(
get_direction_value(head) <> get_direction_value(tail_head)
)
tail_tail_head_is_keyword = is_keyword?(tail_tail_head)
cond do
single_word_direction != "" && next_is_direction &&
tail_tail_head_is_keyword ->
{single_word_direction, tail}
single_word_direction != "" && next_is_direction &&
tail_tail_head == nil ->
{single_word_direction, tail}
single_word_direction != "" && next_is_direction &&
!tail_tail_head_is_keyword ->
{double_word_direction, tail_tail}
single_word_direction != "" && tail == [] ->
{nil, address}
single_word_direction != "" ->
{single_word_direction, tail}
true ->
{nil, address}
end
end
# Parses out the secondary data from an address field and returns
# {secondary_designator, secondary_value, private_mailbox_number,
# leftover_address_list}
defp get_secondary(address) when not is_list(address), do: {nil, nil, nil, []}
defp get_secondary([]), do: {nil, nil, nil, []}
defp get_secondary(address) do
get_secondary(address, address, nil, nil, nil, false)
end
defp get_secondary([], backup, _pmb, _designator, _number, false) do
{nil, nil, nil, backup}
end
defp get_secondary(address, _backup, pmb, designator, value, true) do
[_|tail] = address
cond do
value == nil && pmb != nil ->
clean_designator = safe_replace(designator, ",", "")
clean_pmb = safe_replace(pmb, ",", "")
{clean_designator, nil, clean_pmb, tail}
true ->
clean_designator = safe_replace(designator, ",", "")
clean_value = safe_replace(value, ",", "")
clean_pmb = safe_replace(pmb, ",", "")
{clean_designator, clean_value, clean_pmb, address}
end
end
defp get_secondary(address, backup, pmb, designator, value, false) do
[head|tail] = address
{tail_head, tail_tail} = case length(tail) do
0 -> {"", []}
1 -> {hd(tail), []}
_ -> {hd(tail), tl(tail)}
end
units = AddressUSConfig.secondary_units()
suffixes = AddressUSConfig.street_suffixes()
directions = AddressUSConfig.directions()
cond do
string_is_number?(head) ->
cond do
contains_po_box?(tail) || is_state?(tail_head)->
get_secondary(tail, backup, pmb, designator, value, false)
tail_head == '&' ->
get_secondary(tail_tail, backup, pmb, designator,
tail_head <> " " <> head, false)
safe_starts_with?(value, "&") ->
get_secondary(tail, backup, pmb, designator, head, false)
tail_head == "#" ->
get_secondary(tail_tail, backup, pmb, designator, head, false)
true ->
get_secondary(tail, backup, pmb, designator, head, false)
end
safe_has_key?(units, title_case(head)) ->
cond do
safe_has_key?(suffixes, safe_upcase(value)) ->
get_secondary(backup, backup, nil, nil, nil, true)
true ->
get_secondary(tail, backup, pmb, Map.get(units, head), value, true)
end
Map.values(units) |> Enum.member?(title_case(head)) ->
get_secondary(tail, backup, pmb, title_case(head), value, true)
safe_starts_with?(head, "#") && !contains_po_box?(address) ->
all_unit_values = Map.keys(units) ++ Map.values(units)
cond do
Enum.member?(all_unit_values, title_case(tail_head)) ->
secondary_unit = cond do
Map.values(units) |> Enum.member?(title_case(tail_head)) ->
title_case(tail_head)
true ->
Map.get(units, title_case(tail_head))
end
get_secondary(tail_tail, backup, pmb, secondary_unit,
safe_replace(head, "#", ""), true)
true ->
get_secondary(tail, backup, safe_replace(head, "#", ""),
designator, value, false)
end
value != nil && designator == nil ->
all_unit_values = Map.keys(units) ++ Map.values(units)
cond do
Enum.member?(all_unit_values, title_case(tail_head)) ->
secondary_unit = cond do
Map.values(units) |> Enum.member?(title_case(tail_head)) ->
title_case(tail_head)
true ->
Map.get(units, title_case(tail_head))
end
get_secondary(tail_tail, backup, pmb, secondary_unit,
head <> value, true)
Enum.member?(all_unit_values, title_case(head)) ->
secondary_unit = cond do
Map.values(units) |> Enum.member?(title_case(head)) ->
title_case(head)
true ->
Map.get(units, title_case(head))
end
get_secondary(tail, backup, pmb, secondary_unit, value, true)
true ->
get_secondary(backup, backup, pmb, designator, nil, true)
end
is_possible_suite_number?(tail_head) && (
safe_has_key?(units, title_case(tail_head)) ||
Map.values(units) |> Enum.member?(title_case(tail_head))) ->
get_secondary(tail, backup, pmb, designator,
safe_replace(head, ",", ""), false)
get_suffix_value(tail_head) != nil && get_suffix_value(head) == nil ->
cond do
is_possible_suite_number?(head) && (
String.length(tail_tail) < 2 ||
String.upcase(hd(tail_tail)) == "STATE") ->
get_secondary(backup, backup, pmb, designator, value, true)
Map.values(directions) |> Enum.member?(safe_upcase(head)) ||
safe_has_key?(directions, title_case(head)) ->
get_secondary(backup, backup, pmb, designator, value, true)
true ->
get_secondary(tail, backup, pmb, designator, value, true)
end
tail_head == "&" ->
get_secondary(tail_tail, backup, pmb, designator, value, false)
true ->
get_secondary(backup, backup, pmb, designator, value, true)
end
end
# Parses the state from the address list and returns
# {state, leftover_address_list}.
defp get_state(address) when not is_list(address), do: {nil, nil}
defp get_state([]), do: {nil, nil}
defp get_state(address), do: get_state(address, address, nil, 5)
defp get_state([], backup, _, count) when count > 0, do: {nil, backup}
defp get_state(address, _, state, 0) do
{safe_replace(state, ",", ""), address}
end
defp get_state(address, address_backup, state, count) do
states = AddressUSConfig.states()
[head|tail] = address
state_to_evaluate = safe_replace(merge_names(state, head), ",", "")
cond do
count == 5 && Enum.member?(Map.values(states), head) ->
get_state(tail, address_backup, head, 0)
safe_has_key?(states, state_to_evaluate) ->
get_state(tail, address_backup, Map.get(states, state_to_evaluate), 0)
Enum.member?(Map.values(states), safe_upcase(state_to_evaluate)) ->
get_state(tail, address_backup, safe_upcase(state_to_evaluate), 0)
count == 1 ->
get_state(address_backup, address_backup, nil, 0)
true ->
get_state(tail, address_backup, state_to_evaluate, count-1)
end
end
# Parses the street out of the address list and returns the street name as a
# string.
defp get_street(address) when not is_list(address), do: nil
defp get_street([]), do: nil
defp get_street(address), do: get_street(address, nil, false)
defp get_street([], street, false), do: get_street([], street, true)
defp get_street(_address, street, true) do
corner_case_street_names =
%{"PGA" => "PGA", "ROUTE" => "Route", "RT" => "Route"}
filtered_street = safe_upcase(street) |> safe_replace(~r/\s(\d+)/, "")
directions = AddressUSConfig.directions()
rev_directions = AddressUSConfig.reversed_directions()
cond do
safe_has_key?(corner_case_street_names, filtered_street) ->
street_name = Map.get(corner_case_street_names, filtered_street)
|> safe_replace(~r/\s(\d+)/, "")
street_number = " " <> safe_replace(street, ~r/[a-zA-Z\s]+/, "")
street_name <> street_number |> safe_replace(~r/\s$/, "")
Enum.member?(Map.keys(directions) ++ Map.values(directions),
title_case(street)) ->
cond do
Map.has_key?(directions, title_case(street)) -> title_case(street)
true ->
Map.get(rev_directions, String.upcase(street))
end
true ->
street
end
end
defp get_street(address, street, false) do
[head|tail] = address
cond do
head == "&" || head == "AND" ->
get_street(tail, nil, false)
length(address) == 0 ->
directions = AddressUSConfig.directions()
rev_directions = AddressUSConfig.reversed_directions()
keys = Map.keys(directions)
values = Map.values(directions)
street_is_direction = keys ++ values |> Enum.member?(head)
street_name = cond do
street_is_direction ->
cond do
Map.has_key?(directions, title_case(head)) ->
Map.get(rev_directions, Map.get(directions, title_case(head)))
true ->
Map.get(rev_directions, String.upcase(head))
end
true ->
street
end
get_street(address, street_name, true)
length(clean_hyphenated_street(head)) > 1 ->
cond do
is_keyword?(street) ->
get_street(tail, street <> " " <> head, false)
true ->
get_street(clean_hyphenated_street(head) ++ tail, street, false)
end
true ->
new_address = cond do
street == nil -> title_case(head)
true -> street <> " " <> title_case(head)
end
get_street(tail, new_address, false)
end
end
# Parses the suffix out of the address list and returns
# {suffix, leftover_address_list}
defp get_suffix(address) when not is_list(address), do: {nil, nil}
defp get_suffix([]), do: {nil, nil}
defp get_suffix(address), do: get_suffix(address, nil, false)
defp get_suffix(address, suffix, true), do: {suffix, address}
defp get_suffix(address, _, false) do
[head|tail] = address
new_suffix = get_suffix_value(head)
cond do
Enum.count(clean_hyphenated_street(head)) > 1 ->
get_suffix(address, nil, true)
new_suffix != nil ->
get_suffix(tail, new_suffix, true)
true ->
get_suffix(address, nil, true)
end
end
# Parses an address list for all of the requisite address parts and returns
# a Street module.
# p_val = possible secondary value
# p_des = possible secondary designator
defp parse_address_list(address) when not is_list(address), do: nil
defp parse_address_list([]), do: nil
defp parse_address_list([""]), do: nil
defp parse_address_list(address) do
cleaned_address = Enum.map(address, &(safe_replace(&1, ",", "")))
{designator, value, pmb, address_no_secondary} =
get_secondary(cleaned_address)
{post_direction, address_no_secondary_direction} =
get_post_direction(address_no_secondary)
{suffix, address_no_suffix} =
get_suffix(address_no_secondary_direction)
reversed_address_remnants =
Enum.reverse(address_no_suffix)
{primary_number, box, p_val, p_des, address_no_number} =
get_number(reversed_address_remnants)
{pre_direction, address_no_pre_direction} =
get_pre_direction(address_no_number)
street_name = get_street(address_no_pre_direction)
name = case street_name == nil && !(box == nil) do
true -> box
false -> street_name
end
{final_name, final_secondary_val} = cond do
name == nil ->
cond do
p_val != nil && p_des == nil -> {p_val, nil}
true -> {nil, p_val}
end
true ->
{name, p_val}
end
final_secondary_designator = cond do
designator == nil && p_des != nil -> p_des
true -> designator
end
final_secondary_value = cond do
p_val == nil -> value
true ->
cond do
value == nil -> final_secondary_val
true -> value
end
end
%Street{secondary_designator: final_secondary_designator,
post_direction: post_direction, pre_direction: pre_direction,
secondary_value: final_secondary_value, pmb: pmb, suffix: suffix,
primary_number: primary_number, name: final_name}
end
# Parses postal value passed to it and returns {zip_code, zip_plus_4}
defp parse_postal(postal) when not is_binary(postal), do: {nil, nil}
defp parse_postal(postal) do
cond do
Regex.match?(~r/^\d?\d?\d?\d?\d-\d?\d?\d?\d$/, postal) ->
[dirty_zip|tail] = String.split(postal, "-")
[dirty_plus4|_] = tail
zip = dirty_zip |> safe_replace(",", "") |> String.rjust(5, ?0)
plus4 = dirty_plus4 |> safe_replace(",", "") |> String.rjust(4, ?0)
{zip, plus4}
Regex.match?(~r/^\d?\d?\d?\d?\d$/, postal) ->
clean_postal = postal |> String.rjust(5, ?0) |> safe_replace(",", "")
{clean_postal , nil}
true -> {nil, nil}
end
end
############################################################################
## Helper Functions
############################################################################
# Cleans up hyphenated street values by removing the hyphen and returing the
# values or the appropriate USPS abbreviations for said values in a list.
defp clean_hyphenated_street(value) when not is_binary(value), do: [value]
defp clean_hyphenated_street(value) do
case value |> String.match?(~r/-/) do
true ->
suffix_data = AddressUSConfig.street_suffixes()
suffixes = Map.keys(suffix_data) ++ Map.values(suffix_data)
values = value |> String.split("-")
truths = Enum.map(values, &(Enum.member?(suffixes, safe_upcase(&1))))
new_values = Enum.map(values, fn(v) ->
case safe_has_key?(suffix_data, safe_upcase(v)) do
true -> title_case(Map.get(suffix_data, safe_upcase(v)))
false -> title_case(v)
end
end)
case Enum.any?(truths) do
true -> new_values
false -> [value]
end
false ->
[value]
end
end
# Gets direction abbreviation string.
defp get_direction_abbreviation(value) when not is_binary(value), do: nil
defp get_direction_abbreviation(value) do
val = title_case(value)
directions = AddressUSConfig.directions()
cond do
safe_has_key?(directions, val) -> Map.get(directions, val)
Map.values(directions) |> Enum.member?(val) -> safe_upcase(val)
true -> nil
end
end
# Returns the appropriate direction value if a direction is found.
defp get_direction_value(value) when not is_binary(value), do: ""
defp get_direction_value(value) do
directions = AddressUSConfig.directions()
clean_value = title_case(value)
cond do
safe_has_key?(directions, clean_value) ->
Map.get(directions, clean_value)
Map.values(directions) |> Enum.member?(safe_upcase(clean_value)) ->
safe_upcase(clean_value)
true -> ""
end
end
# Returns the appropriate suffix value if one is found.
defp get_suffix_value(value) when not is_binary(value), do: nil
defp get_suffix_value(value) do
suffixes = AddressUSConfig.street_suffixes()
cleaned_value = title_case(value)
capitalized_keys = Map.keys(suffixes) |> Enum.map(&(title_case(&1)))
capitalized_values = Map.values(suffixes) |> Enum.map(&(title_case(&1)))
suffix_values = capitalized_keys ++ capitalized_values
cond do
Enum.member?(suffix_values, cleaned_value) ->
case safe_has_key?(suffixes, safe_upcase(cleaned_value)) do
true -> Map.get(suffixes, safe_upcase(cleaned_value))
false -> cleaned_value
end
true -> nil
end
end
# Merges two strings into a single string and keeps the spacing correct.
defp merge_names(nil, name2), do: name2
defp merge_names(name1, name2) do
direction1 = get_direction_abbreviation(hd(String.split(name1, " ")))
direction2 = get_direction_abbreviation(name2)
cond do
direction1 != nil && direction2 != nil -> name2 <> name1
name1 == nil -> name2
true -> name2 <> " " <> name1
end
end
# Does a standard safe_upcase, unless the value to be upcased is a nil, in
# which case it returns ""
defp safe_upcase(nil), do: ""
defp safe_upcase(value), do: String.upcase(value)
# Does a standard safe_has_key, unless the value to be checked is a nil, in
# which case it returns false.
defp safe_has_key?(_, nil), do: false
defp safe_has_key?(map, key), do: Map.has_key?(map, key)
# Does a standard String.contains?, unless the value for which to search is
# an empty string, in which case it returns false.
defp safe_contains?(_, ""), do: false
defp safe_contains?(value, k), do: String.contains?(value, k)
# Does a standard safe_replace, unless the value to be modified is a nil in
# which case it just returns a nil.
defp safe_replace(nil, _, _), do: nil
defp safe_replace(value, k, v), do: String.replace(value, k, v)
# Does a standard safe_starts_with?, unless the value to be modified is a nil
# in which case it returns false.
defp safe_starts_with?(nil, _), do: false
defp safe_starts_with?(value, k), do: String.starts_with?(value, k)
# Standardizes the spacing around the commas, periods, and newlines and then
# deletes the periods per the best practices outlined by the USPS. It also
# replaces newline characters with commas, and replaces '# <value>' with
# '#<value>' and then returns the string.
defp standardize_address(address) when not is_binary(address), do: nil
defp standardize_address(address) do
address
|> safe_replace(~r/ United States$/, "")
|> safe_replace(~r/ UNITED STATES$/, "")
|> safe_replace(~r/ US$/, "")
|> safe_replace(~r/US$/, "")
|> safe_replace(~r/\(SEC\)/, "")
|> safe_replace(~r/U.S./, "US")
|> safe_replace(~r/\sM L King\s/, " Martin Luther King ")
|> safe_replace(~r/\sMLK\s/, " Martin Luther King ")
|> safe_replace(~r/\sMLKING\s/, " Martin Luther King ")
|> safe_replace(~r/\sML KING\s/, " Martin Luther King ")
|> safe_replace(~r/(.+)\(/, "\\1 (")
|> safe_replace(~r/\)(.+)/, ") \\1")
|> safe_replace(~r/\((.+)\)/, "\\1")
|> safe_replace(~r/(?i)\sAND\s/, "&")
|> safe_replace(~r/(?i)\sI.E.\s/, "")
|> safe_replace(~r/(?i)\sET\sAL\s/, "")
|> safe_replace(~r/(?i)\sIN\sCARE\sOF\s/, "")
|> safe_replace(~r/(?i)\sCARE\sOF\s/, "")
|> safe_replace(~r/(?i)\sBY\s/, "")
|> safe_replace(~r/(?i)\sFOR\s/, "")
|> safe_replace(~r/(?i)\sALSO\s/, "")
|> safe_replace(~r/(?i)\sATTENTION\s/, "")
|> safe_replace(~r/(?i)\sATTN\s/, "")
|> safe_replace(~r/(?i)\ss#\ss(\S)/, " #\\1")
|> safe_replace(~r/(?i)P O BOX/, "PO BOX")
|> safe_replace(~r/(?i)US (\d+)/, "US Hwy \\1")
|> safe_replace(~r/(?i)(\d+) Hwy (\d+)/, "\\1 Highway \\2")
|> safe_replace(~r/(.+)#/, "\\1 #")
|> safe_replace(~r/\n/, ", ")
|> safe_replace(~r/\t/, " ")
|> safe_replace(~r/\s+/, " ")
|> safe_replace(~r/,(\S)/, ", \\1")
|> safe_replace(~r/\s,(\S)/, ", \\1")
|> safe_replace(~r/(\S),\s/, "\\1, ")
|> safe_replace(~r/\.(\S)/, ". \\1")
|> safe_replace(~r/\s\.\s/, ". ")
|> safe_replace(~r/\s\.(\S)/, ". \\1")
|> safe_replace(~r/(\S)\.\s/, "\\1. ")
|> safe_replace(~r/\./, "")
|> safe_replace(~r/\s,\s/, ", ")
end
# Capitalizes the first letter of every word in a string and returns the
# title cased string.
def title_case(value) when not is_binary(value), do: nil
def title_case(value) do
word_endings = ["ST", "ND", "RD", "TH"]
make_title_case = fn(word) ->
letters = safe_replace(word, ~r/\d+/, "")
cond do
String.downcase(word) == "us" ->
"US"
Regex.match?(~r/^(\d)/, word) && Enum.member?(word_endings, letters) ->
safe_upcase(word)
true ->
String.split(word, "-")
|> Enum.map(&(String.capitalize(&1)))
|> Enum.join("-")
end
end
String.split(value, " ")
|> Enum.map(&(make_title_case.(&1)))
|> Enum.join(" ")
end
# Determines if address list contains a PO Box.
defp contains_po_box?(address) when not is_list(address), do: false
defp contains_po_box?([]), do: false
defp contains_po_box?(address) do
[head|_] = address
full_address = address |> Enum.join(" ") |> safe_upcase
!is_keyword?(head) && String.match?(full_address, ~r/BOX/)
end
# Determines if a value is a number, fraction, or postal keyword.
defp is_keyword?(value) when not is_binary(value), do: false
defp is_keyword?(value) do
word = title_case(value)
units = AddressUSConfig.secondary_units()
suffixes = AddressUSConfig.street_suffixes()
keywords1 = Map.keys(units) ++ Map.values(units) ++ Map.values(suffixes)
keywords2 = Map.keys(suffixes)
cond do
string_is_number_or_fraction?(word) -> true
Enum.member?(keywords1, word) -> true
Enum.member?(keywords2, safe_upcase(word)) -> true
true -> false
end
end
# Detects if a string is a state or not.
defp is_state?(value) when not is_binary(value), do: false
defp is_state?(value) do
state = title_case(value)
states = AddressUSConfig.states()
cond do
safe_has_key?(states, state) -> true
Map.values(states) |> Enum.member?(safe_upcase(state)) -> true
true -> false
end
end
# Determines if a value is a possible Suite value.
defp is_possible_suite_number?(value) do
units = AddressUSConfig.secondary_units()
values = Map.values(units) |> Enum.map(&(String.downcase(&1)))
keys = Map.keys(units) |> Enum.map(&(String.downcase(&1)))
values ++ keys |> Enum.member?(String.downcase(value))
end
# Determines if string can be cleanly converted into a number.
defp string_is_number?(value) when is_number(value), do: true
defp string_is_number?(value) when not is_binary(value), do: false
defp string_is_number?(value) do
is_integer = case Integer.parse(value) do
:error -> false
{_, ""} -> true
{_, _} -> false
end
is_float = case Float.parse(value) do
:error -> false
{_, ""} -> true
{_, _} -> false
end
cond do
is_integer -> true
is_float -> true
true -> false
end
end
# Determines if value is a number or a fraction.
defp string_is_number_or_fraction?(value) when not is_binary(value), do: false
defp string_is_number_or_fraction?(value) do
cond do
string_is_number?(value) ->
true
String.match?(value, ~r/\//) ->
values = String.split(value, "/")
case Enum.count(values) do
2 -> Enum.all?(values, &(string_is_number?(&1)))
_ -> false
end
true -> false
end
end
end