GeoGenius.Providers.SimpleMaps.Rows (geo_genius v0.2.0)

Copy Markdown View Source

Turns one staged SimpleMaps row into the areas it describes.

This is the only module that knows SimpleMaps column names, so a source that renames a column is a change here and nowhere else.

Both files denormalise the hierarchy into every row, so one row is several areas: a uscities row is a city, every county it falls in, and its state; a uszips row is a ZIP, every county it touches, and its state. A ZIP row also names a city, but only as the mailing name USPS prefers for that ZIP, which is why it becomes a name on the ZIP rather than a city area -- the uscities file is where a city gets its identity, and inventing a second one from a ZIP row would key the same city twice.

A state is keyed under the authority that defines its code. Most states key under census and carry an ansi_state code. Six do not. AA, AE and AP are USPS constructs for military mail -- Armed Forces Americas, Europe and Pacific. FM, PW and MH are the Freely Associated States -- Micronesia, Palau and the Marshall Islands -- sovereign countries the USPS serves rather than states of any kind. The Census defines none of the six and assigns none of them an ANSI code, so keying them under census with an ansi_state code would assert two things no source says. They key under usps and carry a usps_state code instead. The code is carried rather than omitted because it is true and it leaves a host the same external-code join every other area offers; an area with no code at all would be reachable only by key. Their area type defaults to state for compatibility, but areas/2 and edges/2 accept the separately declared type selected by the SimpleMaps manifest's non_census_state_area_type option.

Nothing else about them changes: a name is read from state_name the way every other state's is. FM, PW and MH carry one and get it. AA, AE and AP publish nameless: state_name is blank in every one of their uszips rows, and uscities carries no row at all for any of the six codes, so nothing anywhere supplies a name and none is invented to fill the gap. Supplying "Armed Forces Europe" out of a table in this library would manufacture a fact the source does not carry, the same way a centroid copied off a row would. Those three exist so their ZIPs have a parent, and a host that wants a label for them supplies its own.

A county belongs to the state its FIPS is assigned within -- the first two digits of a five-digit county FIPS -- and not to whatever state the row carrying it names. The two disagree wherever a mailing address crosses a state line, and GeoGenius.Providers.SimpleMaps.Fips is where the prefix is read. A row whose county lies in another state yields that state as an area of its own beside the row's, so the hierarchy edges/1 asserts from the same prefix can only name areas areas/1 produced.

A county and a state are read out of columns rather than off rows of their own, so they carry only what those columns say: their code, their name, and their FIPS or ANSI code. They get no centroid, because the row's lat/lng measures the city or ZIP and nothing else; a centroid copied from the row would make areas_near return a fact nobody measured. They get no attributes for the same reason -- the demographic columns of a row describe the city or ZIP, and hanging them on the county would let the last row imported decide what the county's median income is.

edges/2 reads the same county columns areas/2 does, through the same helpers, so the hierarchy this provider asserts can only ever name keys areas/2 produced.

This module knows SimpleMaps column names and how to parse them. GeoGenius.Providers.SimpleMaps.Validation reuses list/2, field/2, label/1, and the two counties-column-name accessors here rather than re-deriving them, so the two modules can never disagree about how a column is read. areas/2 runs Validation.check/1 before building anything, so a row whose county columns contradict each other fails the release instead of being silently reconciled.

Summary

Functions

Returns the areas one staged row describes, in hierarchy order.

Like areas/1, with the area type selected for the six USPS-only state codes.

Returns the relation edges one staged row asserts, parent first.

Like edges/1, with the area type selected for the six USPS-only state codes.

Functions

areas(row)

@spec areas(GeoGenius.Staging.Row.t()) ::
  {:ok, [GeoGenius.Provider.Area.t()]} | {:error, String.t()}

Returns the areas one staged row describes, in hierarchy order.

A uscities row yields its city, its counties, and its state; a uszips row yields its counties, its ZIP, and its state. Any other artifact is an error, since this provider parses only the two files it declares.

Validation.check/1 runs first, so a row whose county columns contradict each other -- a primary county_fips missing from county_fips_all, a counties column of a different length than county_fips_all, or a ZIP's county_weights naming a county set that disagrees with it -- is an error before anything is built from it.

A row whose counties do not all lie in the state it names yields the states they do lie in as well, one area each, read from their FIPS prefixes. A ZIP in the District of Columbia naming a county in Virginia describes Virginia, however little of it, and the county has to hang somewhere.

A row whose city, ZIP, or state code is blank is an error rather than an area: area_key is <authority>:<area_type>:<code>, so a blank code would put every such row under one shared, meaningless key.

That is a deliberate divergence from GeoGenius.Providers.CSV, which returns :skip for a row whose code column is blank. A CSV collection is described by a manifest that may name a column the source populates only sometimes, so skipping is the right read of a gap there. SimpleMaps populates these columns in every row of both files, so a blank one is a corrupt or truncated download rather than a shape to tolerate, and failing the release is better than publishing a hierarchy missing the rows nobody was told about. The error names the column and enough of the row to find it in a file of tens of thousands.

areas(row, non_census_state_area_type)

@spec areas(GeoGenius.Staging.Row.t(), String.t()) ::
  {:ok, [GeoGenius.Provider.Area.t()]} | {:error, String.t()}

Like areas/1, with the area type selected for the six USPS-only state codes.

edges(row)

@spec edges(GeoGenius.Staging.Row.t()) :: [{String.t(), String.t(), String.t()}]

Returns the relation edges one staged row asserts, parent first.

SimpleMaps carries centroids and no boundaries, so nothing here can be measured by overlap; the FIPS columns are the only statement of hierarchy the source makes, and these edges are that statement.

A uscities row asserts one state-to-county edge per county it names and one county-to-city edge per county; a uszips row asserts the same state-to-county edges and a county-to-zip edge per county. The state on a county's edge is the one its own FIPS prefix names, never the one the row carrying it does, and a county whose prefix names no state this provider knows gets no state edge at all rather than a false one. A ZIP row's city column is the mailing name USPS prefers for that ZIP, so no edge is asserted between a city and a ZIP in either direction -- a ZIP crosses city lines, and neither one contains the other.

Every edge names the parent first, the order GeoGenius.Catalog.put_relation/4 takes: a swapped pair writes successfully and silently inverts GeoGenius.children_of/2 and GeoGenius.ancestors_of/2.

A row naming no county asserts one edge instead: its state contains the ZIP directly. A military or Freely Associated States ZIP is in its state and in no county, so the state is the only parent it has, and hanging it there is truer than leaving it with no parent at all. That edge is emitted only for a row with no county -- where counties exist they already connect the ZIP, and a state-to-zip edge beside them would assert a second path to the same place. A uscities row never takes this path, because GeoGenius.Providers.SimpleMaps.Validation.check/1 fails a city row that names no county before this is reached.

Returns [] for an artifact this provider does not parse, since areas/1 is what reports that as an error.

edges(row, non_census_state_area_type)

@spec edges(GeoGenius.Staging.Row.t(), String.t()) :: [
  {String.t(), String.t(), String.t()}
]

Like edges/1, with the area type selected for the six USPS-only state codes.