@@ -139,19 +139,19 @@ def repl_interstate(match: re.Match[str]) -> str:
139139 (r"\bBZN\b" , "BOX" ),
140140 (r"\bB0X\b" , "BOX" ),
141141 (r"\bRUTA RURAL\b" , "RR" ),
142- (r"\bIH(\d+ [A-Z]?)\b" , r"INTERSTATE \1" ),
143- (r"\bI\s?(\d+ [A-Z]?)(?:\s+(?:HIGHWAY|HWY))?\b" , repl_interstate ),
144- (r"\bUS(?:\s+HIGHWAY)?\s+(\d+ [A-Z]?)(?:\s+(?:HIGHWAY|HWY))?\b" , r"US HIGHWAY \1" ),
142+ (r"\bIH(\d{1,4} [A-Z]?)\b" , r"INTERSTATE \1" ),
143+ (r"\bI\s?(\d{1,4} [A-Z]?)(?:\s+(?:HIGHWAY|HWY))?\b" , repl_interstate ),
144+ (r"\bUS(?:\s+HIGHWAY)?\s+(\d{1,4} [A-Z]?)(?:\s+(?:HIGHWAY|HWY))?\b" , r"US HIGHWAY \1" ),
145145 (r"\bBYP ROAD\b" , "BYPASS ROAD" ),
146146 (r"\bKY\s+(\d{1,4})(?:\s+(?:HIGHWAY|HWY))?\b" , r"KY HIGHWAY \1" ),
147147 (r"\bCNTY\b" , "COUNTY" ),
148- (r"\bCR\s+(\d+ [A-Z]?)(?:\s+(?:ROAD|RD))?\b" , r"COUNTY ROAD \1" ),
148+ (r"\bCR\s+(\d{1,4} [A-Z]?)(?:\s+(?:ROAD|RD))?\b" , r"COUNTY ROAD \1" ),
149149 (r"\bHWY\b" , "HIGHWAY" ),
150150 (r"\bRD\b" , "ROAD" ),
151151 (r"\bRT\b" , "ROUTE" ),
152152 (r"\bRTE\b" , "ROUTE" ),
153153 (r"\bSR\s+([A-Z]+)(?:\s+(?:ROUTE|RT|RTE|ROAD|RD))?\b" , r"STATE ROUTE \1" ),
154- (r"\bSR\s+(\d+ [A-Z]?)(?:\s+(?:ROAD|RD|ROUTE|RT|RTE))?\b" , r"STATE ROAD \1" ),
154+ (r"\bSR\s+(\d{1,4} [A-Z]?)(?:\s+(?:ROAD|RD|ROUTE|RT|RTE))?\b" , r"STATE ROAD \1" ),
155155 (r"\bSR\b" , "STATE ROAD" ),
156156 (r"\bTSR\b" , "TOWNSHIP ROAD" ),
157157 ]
@@ -169,8 +169,9 @@ def normalize_text(text: str) -> str:
169169 # Mask floating point periods to preserve them (e.g. 39.2)
170170 text = re .sub (r"(\d)\.(\d)" , r"\1_DOT_\2" , text )
171171
172- # Remove specific punctuation: *, ., (, ), ", :, ;, ', &, @
173- text = re .sub (r'[*.,()":;\'&@]' , "" , text )
172+ # Remove unnecessary punctuation
173+ # Preserve commas since they reduce ambiguity in address components
174+ text = re .sub (r'[*.()":;\'&@]' , "" , text )
174175
175176 # Restore floating point periods
176177 text = text .replace ("_DOT_" , "." )
@@ -306,16 +307,9 @@ def normalize_address(address_str: str) -> str:
306307 else :
307308 formatted_lines .append (line )
308309
309- normalized_lines : list [str ] = []
310- country_line = ""
311- for line in formatted_lines :
312- if line .strip ().upper () in COUNTRIES :
313- country_line = line
314- else :
315- normalized_lines .append (line )
316-
317- if country_line :
318- normalized_lines .append (country_line )
310+ normalized_lines = [
311+ line for line in formatted_lines if line .strip () and line .strip ().upper () not in COUNTRIES
312+ ]
319313
320314 res = "\n " .join (normalized_lines )
321315
@@ -565,7 +559,7 @@ def get_val_simple(obj: object) -> str:
565559 if "SubaddressIdentifier" in tokens :
566560 street_parts .append (tokens ["SubaddressIdentifier" ])
567561
568- # City, State, ZIP — Moved up for boundary standalone passes
562+ # City, State, ZIP — Only move to last_line_parts if they appear together
569563 if "StateName" in tokens :
570564 state_val_dict = _format_state (tokens ["StateName" ])
571565 if "PlaceName" in tokens :
@@ -595,50 +589,6 @@ def get_val_simple(obj: object) -> str:
595589 final_lines .append (_apply_pr_exceptions (" " .join (line1_parts )))
596590 if line2_parts :
597591 final_lines .append (_apply_pr_exceptions (" " .join (line2_parts )))
598- if last_line_parts :
599- # Construct City State Zip carefully
600- city = last_line_parts [0 ] if len (last_line_parts ) > 0 else ""
601- state = last_line_parts [1 ] if len (last_line_parts ) > 1 else ""
602- zip_code_val = last_line_parts [2 ] if len (last_line_parts ) > 2 else ""
603-
604- last_line = ""
605- if city :
606- last_line += city
607-
608- # Handle Canada spacing
609- if state :
610- state_parts = state .split ()
611- if (
612- state_parts [0 ]
613- in (
614- "AB" ,
615- "BC" ,
616- "MB" ,
617- "NB" ,
618- "NL" ,
619- "NT" ,
620- "NS" ,
621- "NU" ,
622- "ON" ,
623- "PE" ,
624- "QC" ,
625- "SK" ,
626- "YT" ,
627- )
628- and len (state_parts ) > 1
629- ):
630- # usaddress merged part of Zip into StateName
631- # e.g., 'ON K1A'
632- state = state_parts [0 ]
633- zip_code_val = " " .join (state_parts [1 :]) + (
634- " " + zip_code_val if zip_code_val else ""
635- )
636-
637- last_line += (" " if last_line else "" ) + state
638-
639- if zip_code_val :
640- last_line += (" " if last_line else "" ) + zip_code_val
641- final_lines .append (last_line )
642592
643593 return "\n " .join (final_lines )
644594
@@ -684,9 +634,26 @@ def _format_from_raw(raw_parsed: list[tuple[str, str]]) -> str:
684634 if can_swap and not any (v .isdigit () for v , _label in raw_parsed [:- 1 ]):
685635 raw_parsed = [raw_parsed [- 1 ], * raw_parsed [:- 1 ]]
686636
637+ # Identify and fix lines that are just City State Zip incorrectly tagged
638+ if len (raw_parsed ) >= 2 :
639+ v_state , l_state = raw_parsed [- 2 ]
640+ _ , l_zip = raw_parsed [- 1 ]
641+ is_state_abbr = v_state .upper () in STATES .values ()
642+ is_state_name = v_state .upper () in STATES
643+ if l_zip == "ZipCode" and (is_state_abbr or is_state_name ) and l_state != "StateName" :
644+ raw_parsed [- 2 ] = (v_state , "StateName" )
645+ for i in range (len (raw_parsed ) - 2 ):
646+ if raw_parsed [i ][1 ] in ("StreetName" , "PlaceName" , "StreetNamePostType" ):
647+ raw_parsed [i ] = (raw_parsed [i ][0 ], "PlaceName" )
648+
687649 has_street_name = any (label == "StreetName" for _val , label in raw_parsed )
688650 reconstructed : list [str ] = []
651+ geo_labels = ("PlaceName" , "StateName" , "ZipCode" , "CountryName" )
652+ has_other = any (p_label not in geo_labels for _ , p_label in raw_parsed )
653+
689654 for p_val , p_label in raw_parsed :
655+ if p_label in geo_labels and not has_other :
656+ continue
690657 # apply directional checks
691658 res_val = p_val
692659 if "Directional" in p_label :
@@ -698,6 +665,7 @@ def _format_from_raw(raw_parsed: list[tuple[str, str]]) -> str:
698665 # apply state checks
699666 elif p_label == "StateName" :
700667 res_val = _format_state (p_val )
668+
701669 # apply secondary unit
702670 elif p_label == "OccupancyType" :
703671 res_val = _format_secondary_unit (p_val )
@@ -722,11 +690,6 @@ def _apply_pr_exceptions(text: str) -> str:
722690 if not lines :
723691 return text
724692
725- last_line_val = ""
726- # If the last line contains a State and Zip code, isolate it from reordering
727- if re .search (r"\b[A-Z]{2}\b\s+\d{5}" , lines [- 1 ].upper ()):
728- last_line_val = lines .pop (- 1 )
729-
730693 urb_lines : list [str ] = []
731694 condo_lines : list [str ] = []
732695 street_lines_list : list [str ] = []
@@ -767,8 +730,7 @@ def _apply_pr_exceptions(text: str) -> str:
767730 break
768731
769732 lines = urb_lines + condo_lines + street_lines_list + other_lines + postal_lines
770- if last_line_val :
771- lines .append (last_line_val )
733+
772734 text = "\n " .join (lines )
773735
774736 # Standardize Spanish boxes on the combined text (e.g., APARTADO -> PO BOX)
0 commit comments