feat: redistribute Canadian owner addresses to match the FAA asset

- publish street, city, postal code and care-of, which the FAA asset already carries as
  registrant_* for 99.7% of US registrants; dropping them here left one repository with
  two different postures on the same class of data
- take the address from the single MAIL_RECIPIENT row rather than merging across parties,
  since a co-owned mark lists several people in different cities

Generated-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ashley Childress
2026-08-31 21:12:00 -04:00
parent bd78cc5b2c
commit fce0b8d18c
3 changed files with 37 additions and 8 deletions
+4 -2
View File
@@ -66,8 +66,10 @@ CCARCS `ACTIVE_FLAG` does **not** mean "current owner": 1,932 currently-Register
`I` parties, and those rows are the `MAIL_RECIPIENT`. Prefer `A` parties where a mark has any, fall
back to all of them otherwise. Filtering on `A` alone publishes registered aircraft with no owner.
Owner mailing addresses (street, city, postal code, care-of) are dropped; name, type, province and
country are published.
Owner mailing addresses **are** published, matching the `registrant_*` address the FAA asset already
carries — the two sources must not diverge on this. Addresses are per-party, so they come from the
single `MAIL_RECIPIENT == "Y"` row (exactly one per mark) and are never merged across co-owners; only
name, type, province and country are merged lists.
## Fork and upstream
+4 -2
View File
@@ -29,8 +29,10 @@ CCARCS download page, so no licence name or URL is asserted here.
construed as constituting an endorsement by the Government of Canada of our
product.
Owner mailing addresses published in the CCARCS export (street, city, postal
code, care-of) are dropped during ingestion and are not redistributed.
Registered-owner mailing addresses are redistributed, matching the registrant
addresses the FAA asset already carries. Because CCARCS lists one row per party,
the published address is that of the single designated mail recipient rather than
a merge across co-owners.
FAA — Releasable Aircraft Database
+29 -4
View File
@@ -34,8 +34,16 @@ CARSOWNR_COLUMNS = [
"OWNER_NAME_OLD_FORMAT", "MAIL_RECIPIENT", "TRIMMED_MARK",
]
# Owner mailing addresses are dropped rather than republished; see NOTICE.
OWNER_PII_COLUMNS = ["STREET_NAME", "STREET_NAME2", "CITY", "POSTAL_CODE", "CARE_OF"]
# Mailing address of the single designated recipient, matching the registrant_* address
# the FAA build already publishes. Addresses are per-party, so they are taken from the one
# MAIL_RECIPIENT row rather than merged across co-owners.
OWNER_ADDRESS_COLUMNS = {
"STREET_NAME": "owner_street_1",
"STREET_NAME2": "owner_street_2",
"CITY": "owner_city",
"POSTAL_CODE": "owner_postal_code",
"CARE_OF": "owner_care_of",
}
FOOTER_RE = re.compile(r"\s*(\d+) rows selected\.\s*")
@@ -125,6 +133,7 @@ def _merge_owners(df_ownr: pd.DataFrame) -> pd.DataFrame:
# Registered marks carry only "I" parties, and those rows are the MAIL_RECIPIENT.
# So prefer active parties where a mark has any, and fall back to all of them
# rather than publishing a registered aircraft with no owner at all.
all_parties = df_ownr
active = df_ownr[df_ownr["ACTIVE_FLAG"].str.upper() == "A"]
marks_with_active = set(active["TRIMMED_MARK"])
df_ownr = pd.concat([
@@ -154,14 +163,25 @@ def _merge_owners(df_ownr: pd.DataFrame) -> pd.DataFrame:
# once several parties share it. Counting distinct names rather than rows keeps this
# consistent with owner_name, which is also deduplicated.
grouped.loc[grouped["owner_party_count"] > 1, "owner_type"] = "Co-owner"
return grouped
# Taken from the unfiltered frame: the designated recipient is the designated
# recipient even when its own party row is flagged inactive.
recipient = (
all_parties[all_parties["MAIL_RECIPIENT"].str.upper() == "Y"]
.drop_duplicates(subset="TRIMMED_MARK", keep="first")
.rename(columns=OWNER_ADDRESS_COLUMNS)
)
return grouped.merge(
recipient[["TRIMMED_MARK", *OWNER_ADDRESS_COLUMNS.values()]],
on="TRIMMED_MARK",
how="left",
)
def convert_tc_ccarcs_to_df(zip_path: Path, date: str) -> pd.DataFrame:
"""Build the OpenAirframes Transport Canada frame from a CCARCS zip."""
df = _read_ccarcs_entry(zip_path, "carscurr.txt", CARSCURR_COLUMNS)
df_ownr = _read_ccarcs_entry(zip_path, "carsownr.txt", CARSOWNR_COLUMNS)
df_ownr = df_ownr.drop(columns=OWNER_PII_COLUMNS)
df = df.merge(_merge_owners(df_ownr), on="TRIMMED_MARK", how="left")
@@ -196,6 +216,11 @@ def convert_tc_ccarcs_to_df(zip_path: Path, date: str) -> pd.DataFrame:
"owner_type": df["owner_type"],
"owner_province_or_state": df["owner_province_or_state"],
"owner_country": df["owner_country"],
"owner_care_of": df["owner_care_of"],
"owner_street_1": df["owner_street_1"],
"owner_street_2": df["owner_street_2"],
"owner_city": df["owner_city"],
"owner_postal_code": df["owner_postal_code"],
"issue_date": df["ISSUE_DATE"],
"effective_date": df["EFFECTIVE_DATE"],
"ineffective_date": df["INEFFECTIVE_DATE"],