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
@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.
@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.
@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.
@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.