1
\$\begingroup\$

Looking for some constructive feedback on Parsing an Address without using Regular Expression in C# for performance purposes.

My aim is to clean the data and to separate it into its respective fields: HouseNumber, Directional, StreetName, StreetType, Zip, City, State, Apartment

Example Input: 123 Main Street

Example Output: HouseNumber: 123, Street Name: Main, Street Type: Street

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace AddressParse
{
    public class ParseAddress
    {
        private List<string> DigitList = new List<string>() { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
        // Populated via: https://usaddress.codeplex.com/SourceControl/latest#trunk/AddressParser/AddressParser.cs
        private List<string> StreetTypesList = new List<string>() { "ALEE", "ALLEY", "ANNEX", "ANEX", "ANX", "ANX.", "ANNX", "ARCADE", "ARC", "AV", "AVE", "AVEN", "AVENU", "AVENUE", "AVN", "AVNUE", "AVN.", "ALY", "ALY.","AVE", "ROAD", "AVE.", "ALLY", "STREET", "ST.", "ST", "WAY", "COURT", "CT", "CT.", "BAYOO", "BYU", "BYU.", "BAYOU", "BEACH", "BCH", "BEND", "BND.", "BND", "BLUF", "BLF", "BLF.", "BLUFF", "BLUFFS", "BLFS", "BOT", "BOT.", "BTM", "BTM.", "BOTTM", "BTTM.", "BOTTOM", "BOUL", "BOUL.", "BLVD.", "BLVD", "BOULEVARD", "BOULV.", "BOULV", "BRANCH", "BR", "BR.", "BRDGE", "BRG.", "BRIDGE", "BRG", "BRNCH", "BROOK", "BROOKS", "BURG", "BG.", "BURGS", "BGS", "BGS.", "BYPA", "BYPA.", "BYP", "BYP.", "CAMP", "CP", "CP.", "CANYN", "CYN", "CYN.", "CANYON", "CAPE", "CPE", "CPE.", "CAUSEWAY", "CSWY", "CSWY.", "CAUSWAY", "CEN.", "CEN", "CTR", "CTR.", "CENT", "CENT.", "CENTER", "CENTERS", "CTRS", "CTRS.", "CENTR", "CENTR.", "CENTRE", "CIRC", "CIRC.", "CIR", "CIR.", "CIRCL", "CIRCL.", "CIRCLE", "CIRCLES", "CIRS", "CIRS", "CK", "CK.", "CRK", "CRK.", "CLIFF", "CLF", "CLF.", "CLIFFS", "CLFS", "CLFS.", "CLUB", "CLB", "CLB.", "CMP", "CMP.", "CP", "CP.", "CNTER", "CTR", "CTR.", "CNTR", "CNTR.", "CNYN", "CYN", "CYN.", "COMMON", "CMN", "CMN.", "CORNER", "COR", "COR.", "CORNERS", "CORS", "CORS.", "COURSE", "CRSE", "CRSE.", "COURT", "CT", "CT.", "COURTS", "CTS", "CTS", "COVE", "CV", "CV.", "COVES", "CVS", "CVS.", "CR", "CRK", "CR.", "CRK.", "CRCL", "CIR", "CRCL.", "CIR.", "CRCLE", "CRECENT", "CRES.", "CREEK", "CRK", "CRK.", "CRESCENT", "CRESENT", "CRES.", "CREST", "CRST", "CRST.", "CROSSING", "XING", "XNG", "XNG.", "CROSSROAD", "XRD", "XRD.", "CRSCNT", "CRES", "CRES.", "CRSENT", "CRSNT", "CRSNT.", "CRSSING", "CRSSNG", "XING.", "CRT", "CRT.", "CT", "CT.", "CURVE", "CURV", "CURV.", "DALE", "DL", "DL.", "DAM", "DM.", "DIV", "DIV.", "DV", "DV.", "DIVIDE", "DRIV", "DRIV.", "DR", "DR.", "DRIVE", "DR", "DR.", "DRIVES", "DRS", "DRS.", "DRV", "DRV.", "DR", "DVD", "DV", "DV.", "ESTATE", "EST", "EST.", "ESTATES", "ESTS", "ESTS.", "EXP", "EXP.", "EXPY", "EXPR", "EXPY", "EXPRESS", "EXPY", "EXPRESSWAY", "EXPY.", "EXPW", "EXTENSION", "EXT", "EXTENSIONS", "EXTS", "EXTN", "EXT", "EXT.", "EXTNSN", "FALLS", "FLS", "FLS.", "FERRY", "FRY", "FRY.", "FIELD", "FLD", "FLD.", "FIELDS", "FLDS", "FLDS.", "FLAT", "FLT", "FLT.", "FLATS", "FLTS", "FORD", "FRD", "FRD." , "FORDS", "FRDS", "FOREST", "FRST", "FORESTS", "FRST", "FORG", "FRG", "FRG.", "FORGE", "FORGES", "FRGS", "FRGS.", "FORK", "FRK", "FRK.", "FORKS", "FRKS", "FORT", "FT", "FT.", "FREEWAY", "FWY", "FREEWY", "FWY.", "FRRY", "FRY", "FRY.", "FRT", "FRWAY", "FWY", "FRWY", "FWY", "FWY.", "GARDEN", "GDN", "GDN.", "GARDENS", "GDNS", "GDNS.", "GATEWAY", "GTWY", "GTWY.", "GATEWY", "GATWAY", "GLEN", "GLN", "GLN.","GLENS", "GLNS", "GLNS", "GRDEN", "GRDN", "GRDNS", "GDNS", "GREEN", "GRN", "GRN.", "GREENS", "GRNS", "GRNS.", "GROV", "GROV.", "GRV", "GRV.", "GROVE", "GROVES", "GRVS", "GRVS.", "GTWAY", "GTWY", "GTWY.", "HARB", "HBR", "HBR.", "HARBOR", "HARBORS", "HBRS", "HARBR", "HAVEN", "HVN", "HVN.", "HAVN", "HEIGHT", "HTS", "HTS.", "HEIGHTS", "HGTS", "HIGHWAY", "HWY", "HWY.", "HIGHWY", "HILL", "HL", "HL.", "HILLS", "HLS", "HLS.", "HIWAY", "HIWY", "HLLW", "HOLW", "HOLLOW", "HOLW.", "HOLLOWS", "HOLWS", "HRBOR", "HBR", "HT", "HTS", "HT.", "HTS.", "HWAY", "INLET", "INLT", "ISLAND", "IS", "IS.", "ISLANDS", "ISS", "ISS.", "ISLES", "ISLE", "ISLND", "ISLNDS", "JCTION", "JCT", "JCT.", "JCTN", "JCTNS", "JCTS", "JUNCTION", "JUNCTIONS", "JUNCTN", "KNOL", "KNL", "KNL.", "KNOLL", "KNOLLS", "KNLS", "LN", "LAKE", "LK", "LK.", "LAKES", "LKS", "LANDING", "LNDG", "LANE", "LANES", "LN", "LDGE", "LDG", "LDG.", "LIGHT", "LGT.", "LIGHTS", "LGTS.", "LNDNG", "LNDG", "LNDG.", "LOAF", "LF", "LA", "LA.", "LOCK", "LCK.", "LOCKS", "LCKS", "LODG", "LDG.", "LODGE", "LDG", "LDG.", "KEY", "KY.", "KY", "LOOPS", "LOOP", "MANOR", "MNR", "MNR.", "MANORS", "MNRS", "MNRS.", "MEADOW", "MDW", "MDW.", "MEADOWS", "MDWS", "MDWS.", "MEDOWS", "MILL", "ML", "ML.", "MILLS", "MLS", "MLS.", "MISSION", "MSN", "MSN.", "MISSN", "MSN", "MSN.", "MNT", "MNT.", "MT", "MT.", "MNTAIN", "MNTN", "MNTN.", "MNTNS", "MTNS", "MOTORWAY", "MTWY", "MOUNT", "MOUNTAIN", "MTN", "MOUNTAINS", "MTNS", "MOUNTIN", "MTN.", "MSSN", "MSN", "MSN.", "MTIN", "MTN", "NECK", "NCK", "NCK.", "ORCHARD", "ORCH", "ORCHRD", "ORCH", "OVERPASS", "OPAS", "OVL", "OVL.", "OVAL", "PARKS", "PARK", "PARKWAY", "PKWY", "PARKWAYS", "PKWY.", "PARKWY", "PASSAGE", "PSGE", "PATHS", "PATH", "PIKES", "PIKE", "PINE", "PNE", "PNE.", "PINES", "PNES", "PK", "PARK", "PKWAY", "PKWY", "PKWYS", "PKWY", "PKY", "PKWY", "PLACE", "PL", "PLAIN", "PLN", "PLAINES", "PLNS", "PLAINS", "PLNS", "PLAZA", "PLZ", "PLZA", "PLZ", "POINT", "PT", "POINTS", "PTS", "PORT", "PRT", "PORTS", "PRTS", "PRAIRIE", "PR.", "PRARIE", "PR", "PRK", "PARK", "PRR", "PR", "RAD", "RADL", "RADIAL", "RADL", "RADIEL", "RADL", "RANCH", "RNCH", "RANCHES", "RNCH", "RAPID", "RPD", "RPD.", "RAPIDS", "RPDS", "RDGE", "RDG", "RDG.", "REST", "RST", "RST.", "RIDGE", "RIDGES", "RDGS", "RIVER", "RIV", "RIV.", "RIVR", "RNCHS", "RNCH", "ROAD", "RD", "RD.", "ROADS", "RDS", "RDS.", "ROUTE", "RTE", "RTE.", "RVR", "RVR.", "RIV", "RIV.", "SHOAL", "SHL", "SHL.", "SHOALS", "SHLS", "SHOAR", "SHR", "SHOARS", "SHRS", "SHORE", "SHR", "SHORES", "SHRS", "SKYWAY", "SKWY", "SPNG", "SPG", "SPNGS", "SPGS", "SPRING", "SPRINGS", "SPRNG", "SPG.", "SPRNGS", "SPGS" , "SPURS", "SPUR", "SQR", "SQ", "SQRE", "SQ.", "SQRS", "SQS", "SQU", "SQU.", "SQUARE", "SQUARES", "SQS.", "STATION", "STA", "STA.", "STATN", "STA", "STN", "STN.", "STA.", "STR", "STR.", "ST", "ST.", "STRAV", "STRA", "STRAVE", "STRA.", "STRAVEN", "STRAVENUE", "STRAVN", "STREAM", "STRM", "STRM.", "STREET", "STREETS", "STS", "STS.", "STREME", "STRM", "STRT", "STRT.", "STRVN", "STRA", "STRVNUE", "SUMIT", "SMT", "SMT.", "SUMITT", "SUMMIT", "TERR", "TERR.", "TER", "TERRACE", "TER.", "THROUGHWAY", "TRWY", "TPK", "TPK.", "TPKE", "TR", "TR.", "TRL", "TRL.", "TRACE", "TRCE", "TRACES", "TRCE", "TRACK", "TRAK", "TRACKS", "TRAK", "TRAFFICWAY", "TRFY", "TRAIL", "TRL", "TRL.", "TRAILS", "TRK", "TRK.", "TRAK", "TRKS", "TRAK", "TRLS", "TRNPK", "TPKE", "TRPK", "TPKE", "TUNEL", "TUNL", "TUNLS", "TUNL.", "TUNNEL", "TUNNELS", "TUNNL", "TURNPIKE", "TPKE", "TURNPK", "TPKE.", "UNDERPASS", "UPAS", "UNION", "UN", "UN.", "UNIONS", "UNS", "UNS.", "VALLEY", "VLY", "VLY.", "VALLEYS", "VLYS", "VLYS.", "VALLY", "VDCT", "VIA", "VIADCT", "VIADUCT", "VIA.", "VIEW", "VW", "VW.", "VIEWS", "VWS", "VWS.", "VILL", "VLG", "VILLAG", "VLG.", "VILLAGE", "VILLAGES", "VLGS", "VILLE", "VL", "VL.", "VILLG", "VILLIAGE", "VIST", "VIST.", "VIS", "VISTA", "VIS.", "VLLY", "VLY", "VLY.", "VST", "VST.", "VSTA", "WALKS", "WALK", "WELL", "WL", "WL.", "WELLS", "WLS", "WLS.","WY", "WY.", "WAY" };
        // Populated via: https://en.wikipedia.org/wiki/Category:Apartment_types
        private List<string> ApartmentList = new List<string>() { "APT", "APT.", "APARTMENT", "HOTEL", "BASEMENT", "BASEMNT", "BEDSIT", "BUSINESS", "CHAMBRE DE BONNE", "CONDO", "CONDOMINIUM", "COMMUNUAL", "LOFT", "LOW-RISE", "ROOM", "RM", "RM.", "UNIT", "UNT", "UNT.", "#", "#." };
        private List<string> DirectionalList = new List<string>() { "N", "W", "E", "S", "N.", "E.", "S.", "W.", "NW", "NW.", "NE", "NE.", "NORTHWEST", "NORTHEAST", "SOUTHWEST", "SOUTHEAST", "NORTH", "EAST", "WEST", "SOUTH" };

        private List<int> NumbersToRemove = new List<int>();
        private List<string> AllUSAZipsList = new List<string>();

        private int numberOfElementsInAddress = 0;
        private string potentialHouseNumberFirstChar = "";
        private string houseNumber = "";
        private string streetTypeFound = "";
        private string apartmentFound = "";
        private string inputAddressU = "";
        private string zipcode = "";
        private string directional = "";
        private int streetTypePos1;
        private int streetTypePos2;
        private int directionalPos1;
        private int directionalPos2;
        private int apartmentPos1;
        private int apartmentPos2;
        private int zipPos1;
        private int zipPos2;
        private int statePos1;
        private int statePos2;
        private int cityPos1;
        private int cityPos2;
        private int fullStatePos1;
        private int fullStatePos2;
        private int houseNumberPos2;
        private int zipIndex;
        private int stringLength;
        private bool gotHouseNumber;
        private bool gotDirectional;
        private bool gotZip;
        private bool gotStreetType;
        private bool gotApartment;
        private bool gotZipState;
        private bool gotZipFullState;
        private bool gotZipCity;
        public string HouseNumber { get; set; }
        public string Directional { get; set; }
        public string StreetName { get; set; }
        public string StreetType { get; set; }
        public string Zip { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Apartment { get; set; }
        public string FullState { get; set; }

        public void parseAddress(string inputAddress)
        {

            inputAddressU = inputAddress.ToUpper();
            // Splits the incoming address into an array of strings seperated by space(s)
            string[] fullAddress = inputAddressU.Split(' ', ',');
            stringLength = inputAddressU.Length;
            numberOfElementsInAddress = fullAddress.Count();
            Console.WriteLine("Original Address: " + inputAddressU);
            Console.WriteLine("--------------------------------------------------------------------");
            // Test #1 Does the first element's first character in the array of strings contain a numeric value?
            // if so 80/20 we can assume this is a housenumber..

            if (numberOfElementsInAddress > 2 && inputAddress.ElementAt(0) != ' ')
            {
                potentialHouseNumberFirstChar = (fullAddress[0].Substring(0, 1));
                if (DigitList.Any(X => potentialHouseNumberFirstChar.Contains(X)))
                {
                    gotHouseNumber = true;
                    houseNumber = fullAddress[0];
                    HouseNumber = fullAddress[0];
                    houseNumberPos2 = inputAddressU.IndexOf(" ", 0);
                }
            }

            // Test #2 see if this address contains a zip code..
            if (numberOfElementsInAddress > 2)
            {
                foreach (string element in fullAddress)
                {
                    if (element.Length == 5 && DigitList.Any(X => element.Contains(X)) && !element.Equals(HouseNumber) && element[0] >= '0' && element[0] <= '9' && element[1] >= '0' && element[1] <= '9' && element[2] >= '0' && element[2] <= '9' && element[3] >= '0' && element[3] <= '9' && element[4] >= '0' && element[4] <= '9')
                    {
                        // Info from: https://www.aggdata.com/node/86
                        string f = @"text file with a list of all possible USA zip codes";
                        List<string> lines = new List<string>();
                        using (StreamReader r = new StreamReader(f))
                        {
                            string line;
                            while ((line = r.ReadLine()) != null)
                            {
                                lines.Add(line);
                            }
                        }
                        foreach (string s in lines)
                        {
                            AllUSAZipsList.Add(s);
                        }


                        if (AllUSAZipsList.Any(X => element.Contains(X)))
                        {
                            zipIndex = AllUSAZipsList.IndexOf(element);
                            // Now since we have an exact zip code match we can find exact info about a person's city and state
                            // Info from: https://www.aggdata.com/node/86
                            string zipInfoFile = @"text file with a list of corresponding state abbreviations to zip code";
                            List<string> lines2 = new List<string>();
                            using (StreamReader x = new StreamReader(zipInfoFile))
                            {
                                string line;
                                while ((line = x.ReadLine()) != null)
                                {
                                    lines2.Add(line);
                                }
                            }
                            // Info from: https://www.aggdata.com/node/86
                            string zipInfoFile2 = @"text file with list of corresponding full state names to zip code";
                            List<string> lines3 = new List<string>();
                            using (StreamReader y = new StreamReader(zipInfoFile2))
                            {
                                string line;
                                while ((line = y.ReadLine()) != null)
                                {
                                    lines3.Add(line);
                                }
                            }
                            // Info from: https://www.aggdata.com/node/86
                            string zipInfoFile3 = @"text file with list of corresponding cities to zip code";
                            List<string> lines4 = new List<string>();
                            using (StreamReader y = new StreamReader(zipInfoFile3))
                            {
                                string line;
                                while ((line = y.ReadLine()) != null)
                                {
                                    lines4.Add(line);
                                }
                            }

                            State = lines2[zipIndex];
                            if (fullAddress.Contains(State))
                            {
                                gotZipState = true;
                                statePos1 = inputAddressU.IndexOf(lines2[zipIndex]);
                                statePos2 = inputAddressU.IndexOf(lines2[zipIndex])+1;
                            }

                            FullState = lines3[zipIndex];
                            if (fullAddress.Contains(FullState))
                            {
                                gotZipFullState = true;
                                fullStatePos1 = inputAddressU.IndexOf(lines3[zipIndex]);
                                fullStatePos2 = inputAddressU.IndexOf(" ",fullStatePos2);
                            }

                            City = lines4[zipIndex];
                            if (fullAddress.Contains(City))
                            {
                                gotZipCity = true;
                                cityPos1 = inputAddressU.IndexOf(lines4[zipIndex]);
                                cityPos2 = inputAddressU.IndexOf(" ", cityPos2);
                            }
                            gotZip = true;
                            zipcode = element;
                            Zip = element;
                            zipPos1 = (inputAddressU.IndexOf(zipcode));
                            zipPos2 = inputAddressU.IndexOf(" ", zipPos1); 
                        }
                    }
                }
            } 

            // Test #3 see if this address contains a directional..
            // Make sure that directional is not last element in array..
            if (numberOfElementsInAddress > 2 && inputAddress.ElementAt(0) != ' ')
            {
                foreach (string element in fullAddress)
                {
                    if (DirectionalList.Any(X => element.Equals(X)))
                    {
                        gotDirectional = true;
                        Directional = element;
                        directionalPos1 = (inputAddressU.IndexOf(directional));
                        directionalPos2 = inputAddressU.IndexOf(" ", directionalPos1);
                    }
                }
            }



            // Test #4 see if this address contains a street type
            if (numberOfElementsInAddress > 2 && inputAddress.ElementAt(0) != ' ')
            {
                foreach (string element in fullAddress)
                {
                    if (StreetTypesList.Any(X => element.Equals(X)))
                    {
                        gotStreetType = true;
                        streetTypeFound = element;
                        StreetType = element;
                        streetTypePos1 = (inputAddressU.IndexOf(streetTypeFound));
                        streetTypePos2 = inputAddressU.IndexOf(" ", streetTypePos1);


                    }
                }
            }

            // Test #5 see if this address contains apartment information..
            if (numberOfElementsInAddress > 2 && inputAddress.ElementAt(0) != ' ')
            {
                foreach (string element in fullAddress)
                {
                    if (ApartmentList.Any(X => element.Contains(X)))
                    {
                        gotApartment = true;
                        apartmentFound = element;
                        apartmentPos1 = (inputAddressU.IndexOf(apartmentFound));
                        apartmentPos2 = inputAddressU.IndexOf(" ", apartmentPos1);
                        Apartment = inputAddressU.Substring(apartmentPos1, stringLength-apartmentPos1);
                    }
                }
            }

       // Logic to determine which elements from array to remove..
         if (gotHouseNumber == true)
            {
                if (houseNumberPos2 == -1)
                {
                    houseNumberPos2 = inputAddressU.Length;
                }

                for (int i = 0; i < houseNumberPos2; i++)
                {
                    NumbersToRemove.Add(i);
                }
            }

            if (gotDirectional == true)
            {
                if (directionalPos2 == -1)
                {
                    directionalPos2 = inputAddressU.Length;
                }

                for (int i = directionalPos1; i < directionalPos2; i++)
                {
                    NumbersToRemove.Add(i);
                }
            }



            if (gotStreetType == true)
            {
                if (streetTypePos2 == -1)
                {
                    streetTypePos2 = inputAddressU.Length;
                }

                for (int i = streetTypePos1; i < streetTypePos2; i++)
                {
                    NumbersToRemove.Add(i);
                }
            }



            if (gotZip == true)
            {
                if (zipPos2 == -1)
                {
                    zipPos2 = inputAddressU.Length;
                }

                for (int i = zipPos1; i < zipPos2; i++)
                {
                    NumbersToRemove.Add(i);
                }
            }

            if (gotApartment == true)
            {
                if (apartmentPos2 == -1)
                {
                    apartmentPos2 = inputAddressU.Length;
                }

                for (int i = apartmentPos1; i < inputAddressU.Length; i++)
                {
                    NumbersToRemove.Add(i);
                }
            }

            if (gotZipState == true)
            {
                if (statePos2 == -1)
                {
                    statePos2 = inputAddressU.Length;
                }
                //Only 2 chars..
                NumbersToRemove.Add(statePos1);
                NumbersToRemove.Add(statePos2); 
            }

            if (gotZipFullState == true)
            {
                if (fullStatePos2 == -1)
                {
                    fullStatePos2 = inputAddressU.Length;
                }
                for (int i = fullStatePos1; i < fullStatePos2; i++)
                {
                    NumbersToRemove.Add(i);
                }
            }

            if (gotZipCity == true)
            {
                if (cityPos2 == -1)
                {
                    cityPos2 = inputAddressU.Length;
                }
                for (int i = cityPos1; i < cityPos2; i++)
                {
                    NumbersToRemove.Add(i);
                }
            }



            // Calculates remaining info and assumes it is the Street Name.. after all else is removed..
            string streetName = string.Concat(inputAddressU.Select((x, i) => NumbersToRemove.Contains(i) ? string.Empty : x.ToString()));
            char[] charsToTrim = { ' ', '\t', ',', };
            StreetName = streetName.Trim(charsToTrim);

        }
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ How is this going to handle for addresses in Salt Lake City like 2950 W 2150 S? (Each address has two numbers, one east/west, one north/south.) \$\endgroup\$ Commented Apr 9, 2016 at 15:34
  • \$\begingroup\$ I love your approach. However, I am curious how you handle this address. "111 South Circle Dr". Circle and Dr are both street types \$\endgroup\$ Commented Dec 20, 2023 at 15:11

1 Answer 1

3
\$\begingroup\$

What kind of performance requirements do you have? Have you tested regexp performance before writing this parsing? Do you know that regexp allows expressions to be compiled and reused, thus it may have similar performance to the parsing code written manually. I would prefer to try something already available (e.g. https://usaddress.codeplex.com/) and write something myself only if I'm not satisfied with it...

Regarding your code:

  • In general the code is hard to read and maintain. Break it down to smaller methods with meaningful names.
  • Replace the usage of DigitList with Char.IsDigit method
  • Replace all lookup list types with HashSet<T> and search for entry with HashSet<T>.Contains instead of .Any(Predicate)- it should slightly improve lookup performance.
  • population of AllUSAZipsList and other lists per each lookup is probably the biggest performance killer, not to mention the memory leak (you do not clear those lists, so on each lookup AllUSAZipsList gets another N copies of all zip codes). Populate all dictionaries on first usage (startup).
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.