Forward geocoding — an address string in, a coordinate out — is
geocode(). It parses each string with
normalize_address(), resolves the result against NAR, and
hands back one row per input in input order, saying for each one
how it was found and what that
cost.
This vignette covers the whole path: the basic call, reading the
columns that qualify every result, choosing which methods to try,
constraining the search, and checking an answer against a second source.
The parsing step has a vignette of its own —
vignette("address-normalization") — since normalizing
addresses is useful whether or not a coordinate comes out at the
end.
No database connection is opened here, and none of the
geocode() calls below take one. geocode()
opens its own when con is not supplied — and, since opening
a 5 GB database costs about half a second, keeps it open for the next
call rather than closing it. So a loop over ten thousand addresses pays
for the connection once, and nothing has to be threaded through to make
that happen.
open_nar() opens that connection up front and
close_nar() ends it. Neither is required; they are there to
name a release other than the latest without repeating it at every call
site, and to control when the database is held — an import needs the
write lock, so rqa_import(), rnf_import() and
nar_connection(refresh = TRUE) close the connection
themselves and say so.
Once a connection is open, version = "latest" means
the release that is open, not whatever StatCan has published
since. Name a version to move to a different one.
Passing con explicitly still does something the implicit
connection cannot: it says unambiguously which database answered. And
for normalize_address() it is not about cost at all — there
con is the switch that turns NAR’s street gazetteer on, so
it changes the answer rather than the speed. That is why the one example
below that opens a connection is a parsing example.
The basic call
The eight addresses below are deliberately awkward: a unit, a lowercased province spelled out, a French street, a prairie range road, a New Brunswick route, a typo, and a post office box.
addresses <- c(
"1055 W Georgia St, Vancouver BC",
"302-1055 west georgia street, vancouver, british columbia",
"100 Queen St W, Toronto, ON M5H 2N2",
"845, rue de Vernon, Gatineau, QC",
"34221 Range Road 272, Red Deer County, AB",
"5491 Route 11, Brantville, NB",
"29 HPCKING AVE, SAULT STE. MARIE, ON",
"PO Box 40, Iqaluit, NU"
)
g <- geocode(addresses)
g |>
mutate(input = substr(input, 1, 38),
uncertainty_m = round(uncertainty_m, 1)) |>
select(input, match_method, uncertainty_m, lon, lat)
#> input match_method uncertainty_m lon lat
#> 1 1055 W Georgia St, Vancouver BC nar_building 0.0 -123.12141 49.28529
#> 2 302-1055 west georgia street, vancouve nar_building 0.0 -123.12141 49.28529
#> 3 100 Queen St W, Toronto, ON M5H 2N2 nar_building 0.0 -79.38250 43.65150
#> 4 845, rue de Vernon, Gatineau, QC nar_building 0.0 -75.81013 45.45202
#> 5 34221 Range Road 272, Red Deer County, nar_building 0.0 -113.73504 51.91683
#> 6 5491 Route 11, Brantville, NB nar_interpolated 83.9 -64.94581 47.40144
#> 7 29 HPCKING AVE, SAULT STE. MARIE, ON nar_building 0.0 -84.36311 46.53467
#> 8 PO Box 40, Iqaluit, NU none NA NA NASeven of the eight resolved, and the one that did not is the one that never could: NAR is a list of civic addresses and holds no post office boxes at all.
Several of these took work that is worth noticing.
HPCKING is not a street — the gazetteer matched it against
the streets NAR actually has in Sault Ste. Marie and returned
Hocking. west georgia street and
W Georgia St both landed on the same canonical street.
Range Road 272 survived intact instead of being read as a
street called Range of type RD. And the
Gatineau address kept its type in front of the name, where French puts
it.
Batch rather than loop. The street-name join is a scan whose cost every row in a call shares — about 0.05 s for a 5-row probe and 0.08 s for a 200-row one — so one call with a thousand addresses is worlds away from a thousand calls.
Every result says how it was found
match_method is the column to read before anything
else.
| value | meaning |
|---|---|
nar_building |
the civic number is in NAR with its own building point |
nar_blockface |
in NAR, but only a blockface centroid is available |
nar_interpolated |
not in NAR; placed between the flanking civic numbers |
nar_no_geometry |
in NAR — ADDR_GUID names the record — but
unplaceable |
not_covered |
in a province this database does not hold |
none |
not found |
not_covered only ever appears against a database
imported for selected provinces
(nar_connection(provinces = )). It is kept apart from
none because it says nothing about the address: no tier
could have matched it, so the parse was never tested.
uncertainty_m prices each of those: it is the
90th-percentile error this package adds relative to NAR’s own
building point. Zero for an exact match, 176 m for a blockface
one (the measured p90 building-to-blockface separation), and half the
flanking span for an interpolated one.
That last figure is not a guess either. The ratio of interpolation
error to flanking span is scale-invariant — its 90th percentile is 0.50
in every span bucket from under 50 m to over 2 km — so half the span is
the p90 error whatever the scale. Which means uncertainty_m
is a real filter:
What it does not include is NAR’s own error. The
StatCan user guide allows a building representative point to be the road
access point or the driveway, and publishes no accuracy figure, so
uncertainty_m = 0 means “this package added nothing”, not
“this point is exact”.
The third qualifier is n_matches, the number of distinct
points that satisfied the query. Anything above 1 means the address was
ambiguous — most often a street name that was never pinned to a
municipality — and uncertainty_m widens to the distance out
to the furthest rejected candidate.
geocode(c("100 Main St", "100 Main St, Moncton, NB")) |>
select(input, match_method, n_matches, uncertainty_m)
#> input match_method n_matches uncertainty_m
#> 1 100 Main St nar_building 139 4043776
#> 2 100 Main St, Moncton, NB nar_building 1 0The first row is an exact nar_building match and still
worthless: there are 139 of them and the uncertainty is four thousand
kilometres, which is the width of the country. match_method
describes the quality of the match, not whether it was the one
you meant — read n_matches alongside it, or fix the
question with the constraints below.
The place you were given may not be the place you named
n_matches == 1 is the qualifier it is easiest to
over-read. It means one candidate was found, not that the right one was
among the candidates searched — and those two come apart in a specific
way. When the string names a community the address register does not
file mail under, the parser resolves it through the census subdivision
it belongs to, which widens the search from that community to every
community in it. MILFORD, NS becomes all 166 of Halifax
Regional Municipality’s, spread over 127 km. Usually that is the feature
working. Occasionally the only street of that name in the whole regional
municipality is 60 km from the one you meant, and it comes back as a
unique exact match.
So normalize_address() reports whether the municipality
it hands back is the one you wrote, and geocode() passes
the flag through:
geocode(c("1741 Brunswick St, Halifax, NS",
"25 River Rd, Moser River, NS",
"36 Lakeview Dr, Howie Centre, NS")) |>
select(input, MUN_NAME, mun_remapped, mun_evidence, n_matches, uncertainty_m)
#> input MUN_NAME mun_remapped mun_evidence n_matches
#> 1 1741 Brunswick St, Halifax, NS HALIFAX FALSE kept 1
#> 2 25 River Rd, Moser River, NS HALIFAX TRUE copostal 1
#> 3 36 Lakeview Dr, Howie Centre, NS CONQUERALL MILLS TRUE untestable 1
#> uncertainty_m
#> 1 0
#> 2 0
#> 3 118All three are exact building matches with one candidate each. The
second is 118 km from Moser River: there is no River Rd
filed under that community, and the one in Halifax — the same regional
municipality — answered for it. The third is 388 km from Howie Centre,
on the other side of the province.
Beside the flag, mun_evidence records how the
substitution was justified, because not all of them are equally
suspect:
mun_evidence |
what it means |
|---|---|
kept |
not a substitution — the register files the address under the name you wrote |
copostal |
the two names appear on the same full postal code in the register, so they are two labels for one delivery geography |
csd |
the name you wrote is the census subdivision the street sits in — an
amalgamation or a legacy name, Toronto for a street still
mailed to North York
|
untestable |
the name you wrote takes no postal-coded mail at all, so there was nothing to check it against |
unattested |
checked, and nothing corroborated it |
inferred |
you named no municipality; the register determined one |
The first three are the attested ones, and uncertainty_m
leaves them alone. That is a measurement rather than a courtesy: against
an independent reading of the same houses, an attested remap lands at a
90th percentile of 52 m, below the 57 m of addresses whose
municipality was never touched. The other three are floored at 118 m,
their own pooled 90th percentile — which is why the third row above does
not claim the 0 m an exact civic match would otherwise imply.
The scoring uses the same evidence: a substitution nothing corroborates is fined, so it has to be an exact or one-keystroke street name agreeing on everything else the string gave before it can win. That more than halves the errors past 5 km.
None of it is a guarantee, and uncertainty_m is the
weakest part. The second row is attested and still 118 km
wrong: the risk a remap carries lives in a tail no 90th percentile
describes — the unattested classes run 1.6–1.8% past 5 km, the untouched
ones 0.05% — so if a kilometre-scale error is unacceptable, filter on
mun_remapped itself rather than on the metres.
One place, several addresses
n_matches counts points, and beside it
n_records counts the NAR addresses that matched.
They are usually different numbers, and the gap is not noise:
geocode(c("4025 W 38th Ave, Vancouver BC",
"6093 Iona Dr, Vancouver BC",
"49321 Range Road 72")) |>
select(input, match_method, n_matches, n_records, match_postal_code)
#> input match_method n_matches n_records match_postal_code
#> 1 4025 W 38th Ave, Vancouver BC nar_building 1 1 V6N2Y8
#> 2 6093 Iona Dr, Vancouver BC nar_building 1 33 V6T0B2
#> 3 49321 Range Road 72 nar_building 1 19 <NA>All three are unambiguous as places — one point each. Only the first is unambiguous as an address. NAR files every unit of a multi-unit building as its own address at the building’s single coordinate, so the second matched 33 records and the third 19. This is not exotic: 47% of the addresses NAR places share their coordinate with at least one other address.
So a high n_records is not by itself a problem.
Geocoding a building with 33 units to that building’s point is the
correct answer to the question that was asked; unless the input says
which unit it means, there is nothing to choose between them, and
choosing would return the same point anyway. n_records
matters when the collapsed records disagree about something you
were relying on — and the disagreement reported today is the postal
code. The second address keeps its, because all 33 units share it. The
third loses its, because those 19 records carry four postal codes
between them.
The two columns fail differently, and that is why both are there.
n_matches above 1 says the point may be in the wrong place.
n_records above 1 says the point is in the right place but
stands for more than one thing.
Looking at the records
geocode_matches() returns them, one row each:
geocode_matches("49321 Range Road 72") |>
select(match_rank, APT_NO_LABEL, MAIL_MUN_NAME, MAIL_POSTAL_CODE, lon, lat) |>
head(6)
#> match_rank APT_NO_LABEL MAIL_MUN_NAME MAIL_POSTAL_CODE lon lat
#> 1 1 9 BRAZEAU COUNTY T7A2A2 -114.922 53.24723
#> 2 2 7 BRAZEAU COUNTY T7A2A2 -114.922 53.24723
#> 3 3 25 DRAYTON VALLEY T7A1R9 -114.922 53.24723
#> 4 4 2 BRAZEAU COUNTY T7A2A2 -114.922 53.24723
#> 5 5 1 BRAZEAU COUNTY T7A2A2 -114.922 53.24723
#> 6 6 10 BRAZEAU COUNTY T7A1R8 -114.922 53.24723Nineteen units of one property, on one coordinate, split across two
mailing municipalities and four postal codes. The rows are ranked the
way the tier ranks them, so match_rank == 1 is the record
geocode() answered with — the two queries collapse the same
candidate set with the same ordering expression, so that is a guarantee
rather than a coincidence. Past the first row the order carries no
meaning: it is the ADDR_GUID tie-break, which exists to
make the first row reproducible.
The usual way to use it is not to call it on everything. Resolve first, then open up only what collapsed:
addr <- c("49321 Range Road 72", "4025 W 38th Ave, Vancouver BC")
collapsed <- geocode(addr)$n_records > 1
geocode_matches(addr[collapsed])$APT_NO_LABEL
#> [1] "9" "7" "25" "2" "1" "10" "15" "5" "6" "17" "3" "23" "4" "29" "13" "8" "27" "21" "19"It reads the exact NAR tier and nothing else, and takes no
method argument, because no other tier has a set to
enumerate. Interpolation stands between two civic numbers and
resolves to no record; the road network file interpolates along a
segment; the online services return an answer rather than a candidate
set. So an address that only those tiers could place has no matches
here:
nrow(geocode_matches("9999 Jasper Ave, Edmonton, AB"))
#> [1] 0That is the right answer and not a gap — nothing was collapsed,
because nothing was resolved to a record in the first place. It does
mean the result is not aligned with the input the way
geocode()’s is; input_id indexes back into
it.
Naming the unit
The collapse closes when the input says which unit it means:
geocode(c("49321 Range Road 72",
"49321 Range Road 72, Unit 9",
"49321 Range Road 72, Unit 999")) |>
select(input, APT_NO_LABEL, match_method, n_records, match_postal_code)
#> input APT_NO_LABEL match_method n_records match_postal_code
#> 1 49321 Range Road 72 <NA> nar_building 19 <NA>
#> 2 49321 Range Road 72, Unit 9 9 nar_building 1 T7A2A2
#> 3 49321 Range Road 72, Unit 999 999 nar_building 19 <NA>The parsed unit is matched against NAR’s own, so the second row resolves to a single record — and gains the postal code the nineteen-record set had to decline. The third names a unit that property does not have, and is placed anyway, exactly as though it had been written without one.
That fallback is what makes the narrowing safe to apply by default,
and it is not a formality. Across the 5,000 Corporations Canada filings
this package measures itself on, 1,189 addresses supply a unit
and match NAR records — and 27.5% of those units are
not in NAR at the civic number they were written against.
Enforcing the unit would take 327 addresses from placed to unplaced, a
far worse trade than a wide n_records. Where the unit is
there, the narrowing is total: every one of the other 862 collapses to
exactly one record. Over the whole draw, 118,937 matched records become
25,955, and 55 addresses gain a match_postal_code.
geocode_matches() narrows identically, because it is the
same candidate set:
geocode_matches("49321 Range Road 72, Unit 9") |>
select(match_rank, APT_NO_LABEL, MAIL_MUN_NAME, MAIL_POSTAL_CODE)
#> match_rank APT_NO_LABEL MAIL_MUN_NAME MAIL_POSTAL_CODE
#> 1 1 9 BRAZEAU COUNTY T7A2A2Unit labels that are words are translated into the vocabulary the
address files use, so Basement and Sous-sol
both find NAR’s BSMT. Zero padding is not touched —
PH01 and PH1 stay different labels — because
0.20% of NAR’s units are padded, and a rule that unpadded them would
need an opinion about every label that is padded on purpose.
Choosing the methods
method names the tiers to try and the order to try them
in. Each tier is offered only the rows the ones before it left without a
position, so the order is the priority.
addr <- "9999 Jasper Ave, Edmonton, AB"
geocode(addr, method = "nar")$match_method
#> [1] "none"
geocode(addr, method = c("nar", "nar_interpolate"))$match_method
#> [1] "nar_interpolated""nar" alone keeps only the addresses NAR actually
carries, which is the right choice when a false position is worse than
no position. The default pair adds interpolation, and on a 5,000-address
sample of real Corporations Canada registered offices that lifts
coverage from 87.9% to 92.4%.
Interpolation is deliberately conservative. It uses the same
side of the street only — pooling both sides costs a median
35.2 m against 4.2 m — and it refuses to extrapolate
past the last known civic number on a side rather than continuing the
run’s spacing, which scores a respectable 15.1 m median but a 237 m 90th
percentile. Those rows come back none.
There are further tiers below: "rqa", offline and
Quebec-only; "rnf", offline and national, which
interpolates along the road network instead of along NAR’s own
addresses; and "bc", "nrcan" and
"qc", which call online services. "rqa" and
"rnf" both need an import of their own first, and
geocode() says so up front if you name one that is not
there.
Constraining the search
known and within are assertions about where
the address is, not hints. known takes whatever components
you already have, named as the output names them, and every one of them
overrides what the string said, constrains the search, and lands on the
returned row.
geocode("100 Queen St W, Vancouver, BC",
known = list(PROV_ABVN = "ON", CSD_NAME = "Toronto")) |>
select(input, PROV_ABVN, MUN_NAME, CSD_NAME, match_method, lon, lat)
#> input PROV_ABVN MUN_NAME CSD_NAME match_method lon lat
#> 1 100 Queen St W, Vancouver, BC ON TORONTO TORONTO nar_building -79.3825 43.6515The string names Vancouver; known says Toronto, Ontario;
the search runs in Toronto and the row reports Toronto. A result that
reported a municipality different from the one it was constrained to
would misdescribe what was actually searched.
Two kinds of city
MUN_NAME and CSD_NAME are different
questions, and this is where the difference bites. MUN_NAME
is the mailing city — the name on the envelope,
compared straight at NAR’s own. CSD_NAME is the
census subdivision, the administrative unit, resolved
through NAR’s alias set. That matters more than it sounds: NAR files a
great many Toronto addresses under SCARBOROUGH,
ETOBICOKE and NORTH YORK, so
CSD_NAME = "Toronto" reaches them and
MUN_NAME = "Toronto" does not.
The two do not nest — one mailing city can span several jurisdictions
and one jurisdiction carries many mailing cities — so asking for the
wrong one is not a near miss. Supply both and both constrain, which is
how you narrow to one community inside an amalgamated city. A parsed
mailing city that contradicts a CSD_NAME you asserted is
dropped rather than left to veto it.
CSD_NAME is also an output column, and there it means
something weaker: the census subdivision the record that matched happens
to be in, not a jurisdiction the search was confined to. The two are
deliberately not the same claim, so a parse handed back to
geocode() answers exactly as the string did.
The same list is how you hand over an address that never was one
string. An assessment roll with the civic number, the street and the
community in columns of their own can be passed as known
directly, and only the parts you do not have are read off the text.
within takes an sf polygon, an
st_bbox, or a bare c(xmin, ymin, xmax, ymax),
and is close to free — the bounding box is compared against stored
coordinate columns that DuckDB prunes with per-row-group zonemaps rather
than scanning.
downtown <- c(-123.13, 49.28, -123.11, 49.29)
geocode(c("1055 W Georgia St, Vancouver, BC",
"4001 W King Edward Ave, Vancouver, BC"),
within = downtown) |>
select(input, match_method)
#> input match_method
#> 1 1055 W Georgia St, Vancouver, BC nar_building
#> 2 4001 W King Edward Ave, Vancouver, BC noneBoth addresses exist; only one is downtown.
Working with the parse
geocode() returns every column
normalize_address() produced alongside the result, so the
parse is always there to inspect when a row surprises you.
g |>
select(CIVIC_NO, STREET_NAME, STREET_TYPE, STREET_DIR, MUN_NAME, PROV_ABVN,
pattern, parse_source)
#> CIVIC_NO STREET_NAME STREET_TYPE STREET_DIR MUN_NAME PROV_ABVN pattern
#> 1 1055 Georgia ST W VANCOUVER BC civic_street
#> 2 1055 Georgia ST W VANCOUVER BC unit_civic
#> 3 100 Queen ST W TORONTO ON civic_street
#> 4 845 de Vernon RUE <NA> GATINEAU QC french_street
#> 5 34221 Range Road 272 <NA> <NA> RED DEER COUNTY AB numbered_road
#> 6 5491 Route 11 <NA> <NA> BRANTVILLE NB numbered_road
#> 7 29 Hocking AVE <NA> SAULT STE. MARIE ON civic_street
#> 8 NA PO BOX 40 <NA> <NA> IQALUIT NU po_box
#> parse_source
#> 1 gazetteer
#> 2 gazetteer
#> 3 gazetteer
#> 4 gazetteer
#> 5 gazetteer
#> 6 gazetteer
#> 7 gazetteer
#> 8 rulesparse_source says whether the row cleared NAR’s street
gazetteer or fell back to the rules alone, confidence
carries the gazetteer’s score where it applies, and pattern
is the structural shape the string parsed as. A none result
is very often a parse worth reading rather than a missing address — note
row 8 above, where po_box says the input was never going to
resolve, because NAR holds no post office boxes.
Two postal codes
The result carries two postal-code columns, and they answer different
questions. POSTAL_CODE comes from the parse — it is what
the input string said, and is empty for the addresses that were typed
without one. match_postal_code is what the matched
record carries, filled in from the database rather than from the
input:
g |>
mutate(input = substr(input, 1, 38)) |>
select(input, match_method, POSTAL_CODE, match_postal_code)
#> input match_method POSTAL_CODE match_postal_code
#> 1 1055 W Georgia St, Vancouver BC nar_building <NA> V6E0B6
#> 2 302-1055 west georgia street, vancouve nar_building <NA> V6E0B6
#> 3 100 Queen St W, Toronto, ON M5H 2N2 nar_building M5H2N2 <NA>
#> 4 845, rue de Vernon, Gatineau, QC nar_building <NA> J9J3K4
#> 5 34221 Range Road 272, Red Deer County, nar_building <NA> T4G0M4
#> 6 5491 Route 11, Brantville, NB nar_interpolated <NA> E9H2A8
#> 7 29 HPCKING AVE, SAULT STE. MARIE, ON nar_building <NA> P6C2B8
#> 8 PO Box 40, Iqaluit, NU none <NA> <NA>Only the tiers that resolve to a record can fill the second column:
the nar tier and the rqa tier. It then
survives whichever tier ends up placing the row, exactly as
ADDR_GUID does — which is why row 6 above is
nar_interpolated and still carries a postal code. NAR held
that address, with no coordinates for it; the exact tier read the record
and interpolation supplied the position. A row interpolated with no such
hit, and every answer from an online service, reports nothing rather
than copying a neighbour’s: an interpolated point sits between
two addresses that need not share a postal code.
The other empty one is more interesting. Row 3 matched exactly, and
NAR carries 100 Queen St W, Toronto twice — once as
M5H2N1 and once as M5H2N2. NAR holds one row
per address, so a civic number with units contributes many rows, and
about one civic number in seventy spans more than one postal code — 4.2%
of addresses, since the buildings this happens to are large ones.
geocode() does not match on unit, so nothing in the query
says which of those rows was meant, and the column stays empty rather
than reporting a coin flip. The postal code in the input string does not
break the tie either: that is what the address claims, not something the
lookup established.
You can also parse once and geocode repeatedly, which is useful when you want to correct a parse before resolving it, or to try the same addresses under different constraints without paying for the parse each time.
con <- nar_connection()
norm <- normalize_address("29 HPCKING AVE, SAULT STE. MARIE, ON", con = con)
norm$STREET_NAME
#> [1] "Hocking"
geocode(norm)$match_method
#> [1] "nar_building"
DBI::dbDisconnect(con)Normalization is a task in its own right, and
vignette("address-normalization") covers it properly: what
the gazetteer fixes, why canonical forms depend on the province, using
address_pattern() to separate addresses that are wrong from
addresses that were never going to resolve, and matching two lists of
addresses to each other without geocoding anything.
Deciding which errors you would rather make
Every qualifier above tells you something about one answer. Turning them into a policy — these answers I will use, those I will not — is a separate decision, and not one the geocoder can make for you. Which error you would rather make depends on what the points are for: aggregating to a dissemination area and dispatching a vehicle to an address want opposite mistakes.
The asymmetry is worth naming, because it drives everything else
here. A false negative is visible: the row comes back
with NA coordinates, and you can count it. A false
positive is invisible by construction — it looks exactly like a
good answer, and it is 118 km away. So the defaults stay conservative
where an answer is decided and generous where it is reported.
geocode_accept() is where you draw your own line. It
re-runs nothing; it reads the columns the result already carries,
withdraws the coordinates from the rows that fail your bar, and records
which test each one failed:
hard <- c(addresses,
"551 Victoria Dr, Birch Grove, NS",
"36 Lakeview Dr, Howie Centre, NS",
"100 Main St")
g <- geocode(hard)
strict <- geocode_accept(g, attested_only = TRUE, unambiguous = TRUE,
max_uncertainty = 100)
table(strict$rejected_for, useNA = "no")
#>
#> ambiguous unattested_mun
#> 2 1Each test is off by default: method,
refused, attested_only,
unambiguous, postal_code,
max_uncertainty and min_confidence. A row is
charged to the first one it fails, in that order.
A rejected row keeps everything except its position.
ADDR_GUID, match_method,
uncertainty_m and the parse all stay, so you can see what
was turned away and argue with it; only the coordinates go:
strict |>
filter(!is.na(rejected_for)) |>
select(input, match_method, uncertainty_m, n_matches, mun_evidence,
rejected_for, lon)
#> input match_method uncertainty_m n_matches mun_evidence
#> 1 5491 Route 11, Brantville, NB nar_interpolated 83.9406 2 kept
#> 2 36 Lakeview Dr, Howie Centre, NS nar_building 118.0000 1 untestable
#> 3 100 Main St nar_building 4043775.8020 139 <NA>
#> rejected_for lon
#> 1 ambiguous NA
#> 2 unattested_mun NA
#> 3 ambiguous NARows that were never placed are not rejections —
rejected_for stays NA there, so
table(rejected_for) counts what your bar cost and nothing
else.
Because nothing is re-queried, moving the line is cheap, which matters: nobody picks the right bar on the first try, and resolving forty thousand addresses takes minutes.
sapply(c(25, 100, 250, Inf), function(u)
mean(!is.na(geocode_accept(g, max_uncertainty = u)$lon)))
#> [1] 0.5454545 0.6363636 0.7272727 0.8181818Seeing the answers that were refused
The other direction is a match the gazetteer rejected. When it scores a match below its threshold, the row comes back unresolved — and from the outside that is indistinguishable from the street not existing. You get no rejected answer, no score, and no evidence class. That is a false negative with nothing to read.
refusable <- c("551 Victoria Dr, Birch Grove, NS",
"16 Kelry Dr, East Dover, NS",
"1741 Brunswick St, Halifax, NS")
geocode(refusable) |>
select(input, STREET_NAME, MUN_NAME, match_method)
#> input STREET_NAME MUN_NAME match_method
#> 1 551 Victoria Dr, Birch Grove, NS VICTORIA BIRCH GROVE none
#> 2 16 Kelry Dr, East Dover, NS KELRY EAST DOVER none
#> 3 1741 Brunswick St, Halifax, NS Brunswick HALIFAX nar_buildingkeep_refused = TRUE reports them instead:
geocode(refusable, keep_refused = TRUE) |>
select(input, STREET_NAME, MUN_NAME, confidence, mun_evidence, refused_for,
match_method, uncertainty_m)
#> input STREET_NAME MUN_NAME confidence mun_evidence refused_for
#> 1 551 Victoria Dr, Birch Grove, NS Victoria SYDNEY 0.792 unattested mun_swap
#> 2 16 Kelry Dr, East Dover, NS Kelly HALIFAX 0.833 copostal score
#> 3 1741 Brunswick St, Halifax, NS Brunswick HALIFAX 1.000 kept <NA>
#> match_method uncertainty_m
#> 1 nar_interpolated 341.0394
#> 2 nar_building 0.0000
#> 3 nar_building 0.0000Both were real answers all along. refused_for names the
gate each one failed:
-
"mun_swap"— the score cleared the threshold before the municipality-swap penalty and not after. The street matched and the municipality did not, which is exactly the case where you may know something the register does not: Birch Grove and Sydney are both in Cape Breton Regional Municipality, and there is noVictoria Drfiled under Birch Grove. -
"score"— everything else.Kelryis one keystroke from theKelly Drthe register carries, and the near-miss on the name is what put the combined score at 0.833 rather than over 0.85.
Note what uncertainty_m does not do here: the
second row is an exact building match and reports 0 m, because that
column describes the error the method introduces and
knows nothing about whether the street was the right street. The refusal
is the warning; the metres are not.
So the natural pairing is one pass with them and one without —
geocode_accept(refused = FALSE) takes them back out again,
and the difference is what the threshold is buying you:
r <- geocode(hard, keep_refused = TRUE)
c(refused_kept = mean(!is.na(r$lon)),
refused_dropped = mean(!is.na(geocode_accept(r, refused = FALSE)$lon)))
#> refused_kept refused_dropped
#> 0.9090909 0.8181818One limit: only matches that cleared the name similarity gate can be reported this way. That gate is applied inside the database query, so a street name too far from every candidate never comes back at all — which is deliberate, since it is the gate that stops a matching street type from carrying an unrelated street over the line.
How to actually use this
The pattern that works is geocode once with everything on,
then split three ways. Resolve with
keep_refused = TRUE — it costs nothing extra and can only
add rows — and let geocode_accept() do the deciding
afterwards, as many times as it takes:
r <- geocode(hard, keep_refused = TRUE)
a <- geocode_accept(r, refused = FALSE, attested_only = TRUE,
unambiguous = TRUE, max_uncertainty = 100)
a |>
mutate(outcome = case_when(!is.na(lon) ~ "use",
!is.na(rejected_for) ~ "review",
TRUE ~ "not placed")) |>
count(outcome, rejected_for)
#> outcome rejected_for n
#> 1 not placed <NA> 1
#> 2 review ambiguous 2
#> 3 review refused 1
#> 4 review unattested_mun 1
#> 5 use <NA> 6Three outcomes, not two, and the middle one is the reason to bother:
- use — cleared your bar. Coordinates present.
-
review — an answer exists and you turned it down.
rejected_forsays why, and every column the decision rested on is still there. This is the pile worth hand-checking, and it is usually small. - not placed — nothing was ever found. No amount of loosening the bar helps; these need a better address string, a different tier, or nothing.
Filtering with filter(!is.na(lon)) collapses the last
two together and throws away the distinction, which is the one piece of
information you cannot recover afterwards.
Choosing a bar
There is no universally right setting, but the task usually decides it:
| what you are doing | a reasonable starting bar |
|---|---|
| aggregating to a census geography — DA, CSD, health region |
attested_only = TRUE, and nothing else. At that scale
100 m of positional error is invisible and a 118 km municipality swap is
not. |
| mapping points, neighbourhood scale | attested_only = TRUE, unambiguous = TRUE, max_uncertainty = 100 |
| anything address-exact — dispatch, service eligibility, joining to a parcel | add method = "nar_building". Nothing interpolated,
nothing online, nothing that resolved to a street rather than a
building. |
| maximum recall, hand-checked after |
keep_refused = TRUE and no bar at all. Sort by
confidence and work down;
refused_for == "mun_swap" is where local knowledge
pays. |
| matching two address lists to each other | don’t geocode at all —
vignette("address-normalization") does this without a
coordinate. |
Three things to keep in mind while tuning:
-
attested_onlyis the one to reach for first. It is the cheapest test by a wide margin: in a 40,000-address Nova Scotia sample the unverified evidence classes are 2.3% of the rows and roughly a third of everything landing more than five kilometres out. The other tests trade far more recall per error removed. -
A row is charged only to the first test it
fails. So the counts in
table(rejected_for)are not what each test costs on its own — run them one at a time if you need to attribute the loss. -
Don’t use
min_confidenceas a stand-in for positional accuracy. It scores how well the string matched a street name, not where the point landed. The Kelry row above scores 0.833 and sits on an exact building point; the Howie Centre row scores 0.900 and is 388 km out.
Finally, report what the bar cost.
mean(is.na(a$lon)) - mean(is.na(r$lon)) is the share of
addresses you gave up, and it belongs next to whatever the coordinates
were used for — a study that geocodes 94% of its subjects and one that
geocodes 71% of them are not describing the same population.
Geometry
geometry = TRUE returns an sf object
instead of lon/lat columns, with an empty
point for the rows that did not resolve. crs picks the CRS
— EPSG:4326 by default, or NULL to keep NAR’s own projected
storage CRS, in which case distances come out in metres.
pts <- geocode(addresses[1:4], geometry = TRUE)
sf::st_geometry(pts)
#> Geometry set for 4 features
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: -123.1214 ymin: 43.6515 xmax: -75.81013 ymax: 49.28529
#> Geodetic CRS: WGS 84
#> POINT (-123.1214 49.28529)
#> POINT (-123.1214 49.28529)
#> POINT (-79.3825 43.6515)
#> POINT (-75.81013 45.45202)Checking an answer
reverse_geocode() runs the other direction, which makes
it a cheap sanity check on a coordinate you just derived.
geocode("100 Queen St W, Toronto, ON", geometry = TRUE) |>
reverse_geocode(match_radius = 50) |>
select(address, dist) |>
head(3)
#> # A tibble: 3 × 2
#> address dist
#> <chr> <dbl>
#> 1 100 W QUEEN ST, TORONTO M5H2N1 0
#> 2 10-100 W QUEEN ST, TORONTO M5H2N2 0
#> 3 700-65 W QUEEN ST, TORONTO M5H2M5 32.1For British Columbia there is a stronger check available: the
Province of BC publishes its own Address Geocoder, a
parcel-level provincial authority. bc_validate() geocodes a
result again through that service and reports the distance between the
two answers. Where they disagree, BC’s answer is generally the more
reliable of the two — but the two sources are not independent of each
other, so a small distance is weaker evidence than it looks, and the
distances are a way to find suspect rows rather than a measurement of
how accurate NAR is.
This is the one path in the package that sends an address off your machine, and nothing reaches it unless you call one of these functions.
bc <- geocode(c("525 Superior St, Victoria, BC",
"800 Robson St, Vancouver, BC",
"3800 Finnerty Rd, Victoria, BC"))
bc_validate(bc) |>
mutate(bc_dist_m = round(bc_dist_m, 1)) |>
select(input, match_method, bc_match_method, bc_score, bc_dist_m)
#> input match_method bc_match_method bc_score bc_dist_m
#> 1 525 Superior St, Victoria, BC nar_building bc_civic 100 6.9
#> 2 800 Robson St, Vancouver, BC nar_interpolated bc_civic 100 104.1
#> 3 3800 Finnerty Rd, Victoria, BC nar_building bc_civic 96 492.8Read the distances as disagreement, not error. The
two sources define their points differently — NAR’s may be the driveway,
BC’s is a parcel point — so a gap contains both sources’ error plus that
definitional difference. Over 250 BC addresses the median disagreement
on the nar_building tier is about 20 m, which is the number
to keep in mind when reading a uncertainty_m of 0. The
third row above is the University of Victoria, where “the address” is a
kilometre-wide campus and the two services have picked different
sensible points on it — a large disagreement that is nobody’s
mistake.
The same service can also be used as a tier, for the BC addresses NAR cannot place at all:
hard <- c("2912 West Broadway, Vancouver, BC",
"1 Nesters Rd, Whistler, BC",
"7165 Nakiska Dr, Vernon, BC")
geocode(hard)$match_method
#> [1] "none" "none" "none"
geocode(hard, method = c("nar", "nar_interpolate", "bc")) |>
select(input, match_method, uncertainty_m)
#> input match_method uncertainty_m
#> 1 2912 West Broadway, Vancouver, BC bc_civic 20
#> 2 1 Nesters Rd, Whistler, BC bc_street 500
#> 3 7165 Nakiska Dr, Vernon, BC bc_civic 20On a sample of 600 BC addresses that NAR’s own pathway placed 524 of, the tier resolved 75 of the remaining 76 — 31 of them at address level, the rest to a block or a street.
A response from that service is not by itself a match, because it always answers. Ask it for an address that does not exist and it returns the centre of the nearest town, not an error:
bc_geocode("1234 Nonexistentzzz Rd, Victoria, BC") |>
select(match_method, bc_score, bc_precision, bc_address)
#> match_method bc_score bc_precision bc_address
#> 1 none 48 LOCALITY Victoria, BCTwo independent floors apply: the service’s own precision vocabulary
has to name a usable match, and bc_score has to clear
min_score, which defaults to 60. Here it is the score that
rejects it — a LOCALITY answer is a legitimate result when
the service is confident, it is just not an address. Either way a
rejected row keeps its score and its bc_faults, so what was
thrown away stays readable rather than vanishing. See
?bc_geocode.
Quebec’s own register
Quebec publishes its address register, the Répertoire québécois
des adresses, in full — and it carries about 750,000 certified
addresses more than NAR’s Quebec extract does. rqa_import()
loads it into the same database, in tables of its own:
rqa_import() # a ~780 MB download, onceOnce imported, "rqa" is available as a tier. It is
offline, it only ever looks at rows that parsed to Quebec, and it
belongs before interpolation:
On 4,000 Corporations Canada filings with a Quebec address, that takes coverage from 88.5% to 90.1% — but the number to look at is a different one. The share placed on a register point rather than an interpolated one goes from 82.7% to 89.1%. Of the 258 filings the tier places, only 62 were unplaced before; the other 196 were being interpolated between two neighbours and now carry the register’s own coordinate, a median of 26 m away. It costs nothing measurable, because the tier only ever sees the rows NAR left behind.
It is not in the default method, because the tables only
exist if you ran the import — and geocode() will tell you
so, up front, rather than when the tier is first reached.
match_method reports the register’s own positional
class, not one flat label:
geocode(c("3190 Boulevard Laurier Est, Saint-Hyacinthe QC",
"1255 Rue Peel, Montreal QC"),
method = c("nar", "rqa", "nar_interpolate")) |>
select(match_method, uncertainty_m, lon, lat)
#> match_method uncertainty_m lon lat
#> 1 rqa_geocoded NA -72.91517 45.63606
#> 2 nar_building 0 -73.57239 45.49992rqa_building means a building point, and gets
uncertainty_m = 0 for the same reason
nar_building does. rqa_geocoded,
rqa_uncertain and rqa_lot get NA
— the register places 30% of these rows by a method it labels
incertaine itself, and nothing here has measured what that is
worth on the ground. The class is reported so you can filter on it.
One condition: the register is CC-BY 4.0, where NAR
is an open government licence. Anything published from these points has
to carry the attribution, which rqa_attribution()
returns.
The road network file
The single largest thing left unplaced is an address whose street is
not in NAR at all — a subdivision built since the last release, a road
NAR never carried. Statistics Canada’s Road Network
File has every street in the country, with an address range on
each side of each segment, and rnf_import() loads it into
the same database in tables of its own:
rnf_import() # a ~340 MB download, onceOnce imported, "rnf" is available as a tier. It is
offline and national, and it belongs after
interpolation — a point placed between two known civic numbers beats one
placed along a range:
On the 5,000 filings above that takes coverage from 92.4% to 94.3%: 93 of the 379 the offline pair gives up on, a quarter of the residual and the largest recovery any tier here offers.
new_streets <- c("1435 Celebration Dr, Pickering, ON L1W 0C4",
"192037A TWP RD 665, Athabasca County, AB T0A 0M0",
"1545 Maley Dr, Sudbury, ON P3A 4R7")
geocode(new_streets, method = c("nar", "nar_interpolate"))$match_method
#> [1] "none" "none" "none"
geocode(new_streets, method = c("nar", "nar_interpolate", "rnf")) |>
select(match_method, n_matches, uncertainty_m, lon, lat)
#> match_method n_matches uncertainty_m lon lat
#> 1 rnf_interpolated 1 95.0000 -79.07905 43.83198
#> 2 rnf_interpolated 1 413.7086 -112.78776 54.74719
#> 3 rnf_ambiguous 3 NA NA NAThe third row is the tier refusing. When more than
one segment of that name in that municipality has a range containing the
number, there is no way to tell which one was meant, so
match_method is rnf_ambiguous,
n_matches says how many were in contention, and no
coordinate comes back. That is where this file’s gross errors live —
ambiguous rows run a 90th percentile of 1.7 km against 108 m for the
rest — and the refusal costs 7 rows in 5,000.
It is a coarse tier and says so. uncertainty_m is
max(95, 0.35 × len_m) and is never 0: the position is a
fraction along a centreline, offset 13 m to the correct side, not a
building. Against 200,000 NAR building points it lands a median 24.3 m
away — about six times worse than nar_interpolate, which is
why it sits below it — with a 90th percentile of 93.3 m. Filter on
uncertainty_m rather than treating every row alike; the
second row above is a township road long enough that the tier admits to
414 m.
One caution the file itself cannot give you: its ranges carry no provenance flag, so an observed range and one the file imputed are the same bytes. Everything quoted here comes from measuring the file against NAR, which is also why the tier is worth less than the overlap suggests — checked against the filer’s own postal code, rows the tier recovers sit a median 149 m from their urban FSA centroid against 60 m for rows NAR also placed. The addresses NAR cannot place are genuinely harder.
The national geolocator
"nrcan" is the other online tier, backed by NRCan’s geolocator. Unlike
"bc" it covers the whole country, and it needs no local
database at all — which makes it the only tier that can answer before a
NAR release has been imported, and the one that covers provinces a
single-province import does not hold.
It has the same trap as the BC service, in a sharper form: it
always answers, and it gives you no score to disbelieve. Asked
for 1 Rue Notre-Dame Ouest, Montreal, QC its top-ranked
answer is a real, precisely interpolated position on a real Rue
Notre-Dame Ouest — in Lorrainville, 500 km away — with nothing in the
response marking it as a substitution.
So the tier re-parses the address the service hands back and requires
it to agree, component by component, with the one that was sent: same
street name, same civic number, and no contradiction on type, direction,
municipality or province. It applies that test to every
result in the response rather than only the top-ranked one, which costs
nothing — the service returns up to 25 in a single reply — and matters
more often than you would guess. The Montréal address above is in that
same reply, ranked seventh, and it is the one the tier returns. Where
two survive, n_matches says so.
Roughly half of what is asked is still rejected, and a rejected row
keeps a nrcan_reject column naming the component that
disagreed, so what was thrown away stays readable. One value in that
column is not about the address at all: the service drops roughly one
request in twelve with an HTTP 500, so nrcan_geocode()
re-sends them (retries, default 3) and reports the ones
that never came back as request failed rather than as an
address it had no answer for. Of the answers that survive, the median
sits 33 m from NAR’s own building point and the 90th percentile at 115
m, which is what uncertainty_m = 150 reports.
Two caveats worth having before you reach for it. As a fallback for
NAR’s own gaps it is worth much less than the BC tier — it places about
8% of what NAR leaves unplaced, because the addresses NAR lacks are
largely the ones no national compilation has. And it does not reverse
geocode; reverse_geocode() is NAR-backed and runs locally.
See ?nrcan_geocode.
OpenStreetMap, and why it is not a tier
There is a third online service bound in this package, and it is
deliberately not something method can name.
osm_geocode() queries the Nominatim instance
Natural Resources Canada hosts — not the volunteer-funded
nominatim.openstreetmap.org, whose usage policy forbids
bulk geocoding.
osm_geocode("990 Bute St, Vancouver, BC")The reason it is a separate call rather than a tier is the licence,
not the accuracy. OpenStreetMap data is ODbL: attribution plus
share-alike, with obligations that attach to a derived database. Every
other source here is under an attribution-only open licence, and a
default tier would fold a handful of ODbL rows into your result table
and quietly change what you may do with the whole of it. So the choice
is yours to make, and every row carries the service’s own
osm_licence string.
It behaves unlike the other two in one welcome respect: it
refuses. Given an address it does not have, it returns nothing
rather than a plausible substitution, and where it knows the street but
not the civic number it says so rather than offering a point. What is
not yet known is its coverage — OpenStreetMap’s Canadian addresses are
concentrated in cities with municipal open-data imports, and no national
comparison has been run, which is why an osm row reports
uncertainty_m as NA rather than a number
someone made up. See ?osm_geocode.
Where this falls short
Measured on 5,000 Corporations Canada registered offices,
geocode() places 92.4% with the default methods, and 94.3%
with "rnf" appended. The ceiling for a NAR-only pathway was
put at around 93% and is essentially met, so what headroom is left is in
the tiers that reach outside NAR rather than in NAR itself.
The residual decomposes roughly as: 3.7% whose street is not in NAR anywhere in the province, 3.8% where the street exists but the civic number could not be reached even by interpolation, 1.4% that never parsed, and a remainder where the street exists under a municipality that did not match. Read the ranking rather than the shares — the parser and the road network file both eat into it.
The notes that ship with the package carry the measurements behind every figure quoted here, along with what is not built yet:
file.show(system.file("notes", "geocoding-status.md", package = "cangeocode"))
file.show(system.file("notes", "road-network-file.md", package = "cangeocode"))
file.show(system.file("notes", "address-normalization-status.md",
package = "cangeocode"))