"""Split one-line Australian addresses into street / suburb / state / postcode, standardise them, and flag what a person must check.
Postcode-to-state ranges follow the published Australia Post allocation. Usage: python standardise_addresses.py addresses_raw.csv"""
import csv, re, sys
STATES = {"VICTORIA": "VIC", "VIC": "VIC", "NEW SOUTH WALES": "NSW", "NSW": "NSW", "QUEENSLAND": "QLD", "QLD": "QLD", "TASMANIA": "TAS", "TAS": "TAS",
          "SOUTH AUSTRALIA": "SA", "SA": "SA", "WESTERN AUSTRALIA": "WA", "WA": "WA", "ACT": "ACT", "NORTHERN TERRITORY": "NT", "NT": "NT"}
RANGES = {"NSW": [(1000, 2599), (2619, 2899), (2921, 2999)], "ACT": [(200, 299), (2600, 2618), (2900, 2920)], "VIC": [(3000, 3999), (8000, 8999)], "QLD": [(4000, 4999), (9000, 9999)],
          "SA": [(5000, 5999)], "WA": [(6000, 6999)], "TAS": [(7000, 7999)], "NT": [(800, 999)]}
TYPES = {"ST": "St", "STR": "St", "STREET": "St", "RD": "Rd", "ROAD": "Rd", "AVE": "Ave", "AV": "Ave", "AVENUE": "Ave", "PDE": "Pde", "PARADE": "Pde", "CT": "Ct", "CRT": "Ct", "COURT": "Ct"}
STATE_RE = re.compile(r"\b(" + "|".join(sorted((re.escape(k) for k in STATES), key=len, reverse=True)) + r")\b")
def clean(raw):
    s = re.sub(r"\s+", " ", raw.replace(",", " ").upper().replace(".", "")).strip(); s = s.replace("S A ", "SA ").replace("W A ", "WA ").replace("N T ", "NT ")
    s = re.sub(r"\b(S A|W A|N T)\b", lambda m: m.group(1).replace(" ", ""), s)
    pc = re.search(r"\b(\d{3,4})\s*$|\b(\d{3,4})\s+(?=[A-Z ]+$)", s); st = ([m for m in STATE_RE.finditer(s)] or [None])[-1]   # last match, so "Victoria St" is not read as the state
    if not pc or not st: return None, "No state or postcode found"
    code = (pc.group(1) or pc.group(2)).zfill(4); state = STATES[st.group(1)]
    rest = (s[:min(pc.start(), st.start())]).strip()
    m = re.match(r"^((?:\d+/)?\d+[A-Z]?)\s+(.+?)\s+(" + "|".join(TYPES) + r")\s+(.+)$", rest)
    if not m: return None, "Street line not recognised (PO Box or missing street type)"
    street = f"{m.group(1)} {m.group(2).title()} {TYPES[m.group(3)]}"; suburb = m.group(4).title()
    ok = any(a <= int(code) <= b for a, b in RANGES[state])
    return {"street": street, "suburb": suburb, "state": state, "postcode": code}, ("" if ok else f"Postcode {code} is not a {state} postcode")
def main(fn):
    out = []
    for r in csv.DictReader(open(fn)):
        parts, note = clean(r["address"])
        parts = parts or {"street": "", "suburb": "", "state": "", "postcode": ""}
        out.append({"customer_id": r["customer_id"], "address_raw": r["address"], **parts, "status": "Check" if note else "OK", "note": note})
    with open("addresses_clean.csv", "w", newline="") as f: w = csv.DictWriter(f, fieldnames=list(out[0])); w.writeheader(); w.writerows(out)
    print(f"{len(out)} addresses; {sum(o['status'] == 'OK' for o in out)} standardised; {sum(o['status'] == 'Check' for o in out)} flagged")
    for o in out:
        if o["note"]: print("  ", o["customer_id"], o["address_raw"], "->", o["note"])
if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else "addresses_raw.csv")
