One of this package’s seven data sources — the base every other
source here is measured against. vignette("data-sources")
is the overview and puts it in context.
Almost everything in this package reads Statistics Canada’s
National Address Repository.
reverse_geocode() is NAR and nothing else. The first two
geocode() tiers are NAR. normalize_address()
resolves municipalities and street names against gazetteers built from
NAR at import time. And every other source wired up here — the BC
Address Geocoder, NRCan’s geolocator, Quebec’s register, the road
network file — has been measured against NAR, because there was
nothing else national to measure against.
That double role is why NAR’s limits matter twice. A wrong record is both a wrong answer and a wrong yardstick: where NAR is off, a comparison that calls another source wrong may have it backwards.
This vignette is about where NAR falls short and what the package
does about each one. None of it is an argument against using NAR — it is
the best open national address file Canada has, 16.2 of its 17.4 million
addresses carry a building point, and the alternatives are provincial,
proprietary, or both. It is an argument for reading
match_method, n_matches and
uncertainty_m rather than the coordinate alone.
library(cangeocode)
library(dplyr)
con <- nar_connection()What this adds to the package
Everything. NAR is not a tier — it is the base every offline tier
reads and the reference the online ones are checked against.
nar_connection() opens it, reverse_geocode()
is entirely NAR-backed, the "nar" and
"nar_interpolate" tiers are it, the gazetteer
normalize_address() uses is built from its street and
municipality inventory, and collect_nar() hands you the
rows directly. Without an imported NAR release, the only things in this
package that work are the four online geocoders and the rule-based half
of the parser.
Licence
Statistics Canada Open Licence — the same terms as
the Road Network File — and NAR’s own product page carries the
attribution wording. It composes with the CC-BY covering Quebec’s
register, which is what lets geocode() mix all three tiers
into one result table by default, and why the ODbL data behind
osm_geocode() is deliberately excluded from that. See
vignette("source-osm").
How NAR is built, and what it is built from
NAR is not collected. It is extracted from Statistics Canada’s Statistical Building Register (SBgR) — the internal register of every building and building unit in the country — and its User Guide adds that the addresses “were validated by two independent data sources” before release. So the question of what NAR is made of is really a question about the SBgR, and StatCan documents its inputs in two distinct roles:
-
The universe — which addresses exist at all, and
where they are — comes from the national Canada Post
Point-of-Call file together with the provincial 911
emergency files. In Nova Scotia that is the Nova Scotia Civic
Address File; in Quebec, the Répertoire québécois des adresses that
vignette("source-rqa")covers. -
The attributes are complemented from
property assessment rolls and land registries, hydro-electricity
company files, and provincial driver’s licence files, which
populate “building type, building name and building unit usage”. Those
are NAR’s
BU_USEandBU_N_CIVIC_ADDcolumns, and they are populated on every row in every province.
Each source is standardised and then geocoded by StatCan to its own spatial data infrastructure — though “geocoded” turns out to mean less than it sounds like, since where this has been checked the resulting coordinate is the contributor’s own, transformed onto StatCan’s frame rather than re-derived. On top of that, the register is maintained by listing and profiling activities, the Canadian Housing Statistics Program, survey feedback, and — every five years — a reconciliation against the Census of Population, so that dwellings found on the ground during collection get added.
Two things follow that matter when reading the rest of this vignette.
First, the vintage of a record is the vintage of its contributor’s file, not of the NAR release. A subdivision registered after a province’s last 911 extract is absent from NAR no matter how recent the release is, which is most of what the section on missing addresses below is describing.
Second, and less obvious: NAR is not independent of the provincial address authority. It is largely a re-geocoded, standardised assembly of those files. That has been measured in two provinces, and in both the provincial file turns out to be inside NAR rather than beside it:
| province | authority file | NAR’s distance from it |
|---|---|---|
| Quebec | Répertoire québécois des adresses | median 0.21 m over 2.5 M addresses |
| Nova Scotia | NS Civic Address File | median 1.04 m, a single constant re-datum |
In Nova Scotia the entire distribution sits in one 1–2 m band, and subtracting a single vector leaves a residual of about three centimetres — it is the same coordinate in a different NAD83 realization. Comparing NAR to either file measures how well an address parsed, not whether its coordinate is right.
The practical rule this gives, for anyone reaching for a provincial
file to check NAR with: the more authoritative a provincial
address file is, the more likely NAR already contains it. A
useful check has to come from somebody who collected the addresses for a
different reason — which is why the Nova Scotia comparison in
inst/notes/nova-scotia-pvsc.md is built on the province’s
property assessment file rather than its addressing one.
A reference is not ground truth
NAR is a compilation. Statistics Canada assembles it from what provinces, territories and municipalities supply, and the quality of a record is largely the quality of what its contributor sent. StatCan publishes no accuracy figure for it, and the User Guide is explicit that a building representative point “may not correspond exactly to the physical center of the building structure itself” — it can be the road access point or the driveway.
So the number this package reports as uncertainty_m is
the error this package introduced relative to NAR’s own
point, not the distance to the building.
uncertainty_m = 0 means “nothing was added here”, and says
nothing at all about whether NAR put the address in the right place.
The one calibration of that floor we have is 250 BC addresses
geocoded through NAR and again through the BC Address Geocoder, which is
a parcel-level provincial authority: on nar_building
matches the two answers sit a median 19.8 m apart, p90
118.8 m. That is disagreement rather than error — it contains both
sources’ error plus their definitional difference — and the two are not
independent, for the reason the previous section gives: NAR’s BC records
are assembled from BC’s own files. That is not a guess about this
particular pair, it is how the register is built. Read the 19.8 m as a
lower bound on how far apart two genuinely independent sources would
be.
Not every point is the same kind of point
NAR carries two positional fields, and the package prefers the first:
tbl(con, "Addresses") |>
count(geom_source) |>
arrange(desc(n)) |>
collect()
#> # A tibble: 3 × 2
#> geom_source n
#> <chr> <dbl>
#> 1 building 16157303
#> 2 blockface 1140090
#> 3 <NA> 65083BG is Building and BF is
Blockface — the centroid of one side of a street
between two intersecting features. A blockface point is not a coarser
building point; it is a different object, and it is shared: 8.67
addresses per distinct blockface point against 1.61 per building point,
a median 50 m from the building point they stand in for (p95 331 m), and
in the worst case one point carrying 578 addresses.
What the package does. It never reports a flat
“matched”. match_method distinguishes
nar_building, nar_blockface and
nar_no_geometry, and reverse_geocode() returns
geom_source beside dist precisely so the two
are not ranked against each other. A blockface dist of 30 m
and a building dist of 30 m are not the same claim.
Some points are in the wrong place
One postal code makes the case better than a description.
M4W2C9 is a short street in Rosedale, Toronto, with 23
addresses on it:
andrews <- tbl(con, "Addresses") |>
filter(MAIL_POSTAL_CODE == "M4W2C9") |>
select(CIVIC_NO, OFFICIAL_STREET_NAME, geom_source, x, y) |>
collect()
andrews |>
mutate(km_from_the_rest = round(sqrt((x - median(x))^2 + (y - median(y))^2) / 1000, 2)) |>
count(geom_source, km_from_the_rest)
#> # A tibble: 9 × 3
#> geom_source km_from_the_rest n
#> <chr> <dbl> <int>
#> 1 blockface 0 14
#> 2 building 0.02 1
#> 3 building 0.03 1
#> 4 building 0.04 1
#> 5 building 0.05 2
#> 6 building 0.06 1
#> 7 building 0.07 1
#> 8 building 0.08 1
#> 9 building 2.09 1Two of this vignette’s problems are visible in that one table. Fourteen of the 23 sit on a single shared blockface point. And one address — number 7 — carries a building point two kilometres away, which is not a rounding difference or a coarse fallback but a different part of the city:
misplaced <- geocode("7 Saint Andrews Gdns, Toronto, ON")
misplaced |> select(match_method, n_matches, uncertainty_m, lon, lat)
#> match_method n_matches uncertainty_m lon lat
#> 1 nar_building 1 0 -79.38556 43.66869nar_building, one match, uncertainty_m of
0. Everything the package can say about this answer says it is good.
Asking what is actually at that coordinate is what gives it away:
reverse_geocode(c(misplaced$lon, misplaced$lat), match_radius = 150) |>
count(OFFICIAL_STREET_NAME, sort = TRUE)
#> # A tibble: 7 × 2
#> OFFICIAL_STREET_NAME n
#> <chr> <int>
#> 1 Charles 1869
#> 2 Hayden 437
#> 3 Yonge 51
#> 4 Isabella 11
#> 5 Saint Nicholas 3
#> 6 Saint Andrews 1
#> 7 Saint Mary 1Charles, Hayden and Yonge: the point is two thousand addresses deep into downtown, and the only thing within 150 m named Saint Andrews is the misplaced record itself.
Rows like this can be found without a second source,
which is unusual here. A row’s own postal code disagreeing with its own
coordinate is an internal contradiction — no reference dataset
is needed to see it and none can be blamed for it.
inst/notes/nar-consistency.md does exactly that across the
whole file, and the funnel is worth quoting because the shape of it is
the finding:
| rows | |
|---|---|
| addresses with both a postal code and a coordinate | 17,243,149 |
far from every address sharing their postal code (d_own
> 1 km) |
46,679 |
and sitting on top of a different postal code
(d_other < d_own/10) |
17,224 |
still flagged once d_other is measured along the road
network |
13,460 |
| of those, where the coordinate is the part to disbelieve | 653 |
The last step needs a third field, because with a postal code and a coordinate there are only two witnesses and no majority. The street name is that field: ask whether the street NAR names exists around the point or around the postal code’s other addresses. For 8,875 rows the coordinate is corroborated and the postal code is the odd one out; for 653 it is the other way around, and those are the rows where the coordinate is what to disbelieve.
Three cautions come with those numbers, and they are why nothing here is repaired automatically:
- This is a lower bound, and a biased one. It can only see an error that leaves its postal code’s neighbourhood entirely. An address on the wrong side of the street, or on the wrong block of the right street, is invisible to it — and those are certainly more common.
- 84,282 addresses are the only member of their postal code, and cannot be far from a sibling they do not have. The method is blind to them by construction.
- Corroboration is not proof. One address in Amos, Québec has a point in Kitchener, Ontario, 609 km away — and the street test supports it, because Kitchener has an Appalachian Cres and the point sits 15 m from it. A shared street name is exactly what produces the misplacement in the first place.
What the package does. Nothing repairs these, and no
tier reads that analysis — it is a measurement, not a correction. What
is available to a caller is the constraint arguments and the validators:
known and within are assertions that force the
search into a region you know, and bc_validate() and
qc_validate() re-ask a provincial authority about an answer
you already have. vignette("geocoding") covers both.
Addresses NAR does not have
The 2026-06 release carries 17.4 million addresses, and Canada has more than that. Two shapes of absence matter.
The address is missing but the street is not. Then NAR does not fail — it interpolates, and returns a point. Quebec is where this is measurable, because Quebec publishes the register NAR’s own Quebec rows are derived from, and it is about 308,000 addresses larger. Four addresses that are genuinely in the register and genuinely absent from NAR:
gap <- c("5510 Saint-Jacques, Montreal, QC", "1650 Chabanel, Montreal, QC",
"431 Courtemanche, Montreal-Est, QC", "45 Gamelin, Gatineau, QC")
geocode(gap, method = c("nar", "nar_interpolate")) |>
select(input, match_method, uncertainty_m)
#> input match_method uncertainty_m
#> 1 5510 Saint-Jacques, Montreal, QC nar_interpolated 58.76047
#> 2 1650 Chabanel, Montreal, QC none NA
#> 3 431 Courtemanche, Montreal-Est, QC none NA
#> 4 45 Gamelin, Gatineau, QC nar_interpolated 14.25068Two come back none, which is honest. The other two come
back nar_interpolated, placed between the neighbours NAR
does carry — a plausible point for an address NAR has never seen. With
Quebec’s own register imported, all four resolve against the register
instead:
geocode(gap, method = c("nar", "rqa", "nar_interpolate")) |>
select(match_method, uncertainty_m, lon, lat)
#> match_method uncertainty_m lon lat
#> 1 rqa_geocoded NA -73.60463 45.46885
#> 2 rqa_building 0 -73.65830 45.52960
#> 3 rqa_building 0 -73.51165 45.63419
#> 4 rqa_geocoded NA -75.73602 45.44344Two of them land on a register building point, and
two on the register’s own GEOCODEE class — which the tier
reports as rqa_geocoded with an uncertainty_m
of NA, rather than flattening it to a match and a zero.
Quebec certified those two positions differently, and the column says
so.
The street is missing too. Interpolation cannot help
there, and this is the single largest component of what
geocode() fails on. Statistics Canada’s road network file
has every street in the country, which is what the "rnf"
tier interpolates along.
What the package does. rqa_import() and
rnf_import() add those two sources as tiers, both offline,
neither in the default method because their tables only
exist if you ran the import. Both are kept in their own
tables rather than merged into Addresses. That is
deliberate: merging would fabricate NAR records nobody published, and it
would spend the only instrument either source’s coverage can be measured
with.
One warning applies to both, and it generalizes past this package. Quality measured where two sources overlap does not describe the rows where one of them is silent. Checked against the filer’s own postal code, the road network tier sits 60 m from the expected location on rows NAR also placed — and 149 m on the rows only it can place. The addresses NAR is missing are harder addresses, not a random sample of the ones it has.
The string matched more than one row
Not every bad answer is a bad record. A great many are a question that did not identify one address:
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 against 139
addresses, and the uncertainty is four thousand kilometres — the width
of the country. It is not wrong; it is the correct answer to an
under-specified question.
What the package does. n_matches is on
every row, uncertainty_m grows to cover the spread of the
candidates rather than reporting the first one’s precision, and
known / within let you supply what the string
did not. Filtering on uncertainty_m catches this class
before it reaches a map.
The same street, filed two ways
A compass word at the front of a street name is either part of the
name or a direction, and the string itself does not say which.
West Jeddore Rd in Head of Jeddore is a name;
Georgia St W in Vancouver is a direction. NAR has to answer
that question for 3,386 streets, and mostly answers it the same way
twice:
compass <- c("EAST", "WEST", "NORTH", "SOUTH", "NORD", "SUD", "EST", "OUEST")
abbr <- c(EAST = "E", WEST = "W", NORTH = "N", SOUTH = "S",
NORD = "N", SUD = "S", EST = "E", OUEST = "O")
lead <- tbl(con, "Streets") |>
mutate(o_word = split_part(toupper(OFFICIAL_STREET_NAME), " ", 1L),
m_word = split_part(toupper(MAIL_STREET_NAME), " ", 1L)) |>
filter(o_word %in% compass | m_word %in% compass) |>
collect() |>
# which family leads with the word, and what the *other* family would have to
# carry for the two spellings to mean the same street
mutate(o_lead = o_word %in% compass & toupper(OFFICIAL_STREET_NAME) != o_word,
m_lead = m_word %in% compass & toupper(MAIL_STREET_NAME) != m_word,
word = ifelse(o_lead, o_word, m_word),
echo = unname(abbr[word]),
own_dir = ifelse(o_lead, OFFICIAL_STREET_DIR, MAIL_STREET_DIR),
other_dir = ifelse(o_lead, MAIL_STREET_DIR, OFFICIAL_STREET_DIR),
led_name = ifelse(o_lead, OFFICIAL_STREET_NAME, MAIL_STREET_NAME),
other_name = ifelse(o_lead, MAIL_STREET_NAME, OFFICIAL_STREET_NAME)) |>
filter(o_lead | m_lead) |>
mutate(shape = case_when(
own_dir == "" & other_dir == "" ~ "the compass word is simply the name",
o_lead != m_lead & other_dir == echo &
toupper(other_name) == sub("^\\S+ ", "", toupper(led_name)) ~
"the two name families disagree with each other",
own_dir == echo ~ "the direction repeats the word already in the name",
.default = "the name has the word, the direction is another quadrant"))
lead |>
summarise(streets = n(), addresses = sum(N_ADDRESSES), .by = shape) |>
arrange(desc(addresses))
#> # A tibble: 4 × 3
#> shape streets addresses
#> <chr> <int> <dbl>
#> 1 the compass word is simply the name 2948 92167
#> 2 the name has the word, the direction is another quadrant 273 7979
#> 3 the two name families disagree with each other 162 3193
#> 4 the direction repeats the word already in the name 3 25The overwhelming bulk — 92,167 addresses — sit on a street whose name
begins with a compass word and which carries no direction field
on either family. NAR is stating that the word is the name.
That is a usable signal rather than a problem: a parse that reads
EAST UNIACKE RD as UNIACKE plus direction
E can be checked against a database that files the street
as East Uniacke Rd with no direction at all.
The 7,979 addresses that carry both are not contradictions either.
South Terwillegar Dr NW in Edmonton is the South
Terwillegar neighbourhood in the city’s northwest quadrant, and both
halves are load-bearing — a reader that strips the leading word here
gets the name and the direction wrong.
The 3,193 that remain are where NAR contradicts itself. One of its two name families embeds the word and the other splits it into its own direction field, and the two spellings mean the same street:
lead |>
filter(shape == "the two name families disagree with each other") |>
slice_max(N_ADDRESSES, n = 6) |>
transmute(official = trimws(paste(OFFICIAL_STREET_NAME, OFFICIAL_STREET_TYPE,
OFFICIAL_STREET_DIR)),
mail = trimws(paste(MAIL_STREET_NAME, MAIL_STREET_TYPE,
MAIL_STREET_DIR)),
MAIL_MUN_NAME, MAIL_PROV_ABVN, N_ADDRESSES)
#> # A tibble: 6 × 5
#> official mail MAIL_MUN_NAME MAIL_PROV_ABVN N_ADDRESSES
#> <chr> <chr> <chr> <chr> <dbl>
#> 1 South Dogwood ST DOGWOOD ST S CAMPBELL RIVER BC 227
#> 2 South Pine Lake RD PINE LAKE RD S RED DEER COUNTY AB 147
#> 3 Baptiste Lake RD S SOUTH BAPTISTE LAKE RD BANCROFT ON 129
#> 4 Baptiste Lake RD N NORTH BAPTISTE LAKE RD MAYNOOTH ON 117
#> 5 Big Island RD N NORTH BIG ISLAND RD DEMORESTVILLE ON 108
#> 6 East Waseosa Lake RD WASEOSA LAKE RD E HUNTSVILLE ON 76Baptiste Lake Rd S and
SOUTH BAPTISTE LAKE RD are one street in Bancroft, written
both ways in a single row. This is the same species of finding as the
misplaced points above — an internal contradiction, visible
without any second source and not blamable on one — but it is a far
smaller one, and unlike a wrong coordinate it is close to harmless.
What the package does. Nothing repairs these, and
little needs to. The gazetteer behind normalize_address()
scores an input against both NAR name families and keeps the
better match, so a street NAR spells two ways is a street with two
chances to match rather than one: SOUTH DOGWOOD ST finds
the official spelling in Campbell River and DOGWOOD ST S
finds the mail one, and both land on the same street.
The harder half of this problem is not NAR’s. NAR at least records an
answer; a free-text address arrives with none, and
normalize_address() has to decide whether a leading
EAST is a name or a direction before it can look anything
up. It declines to choose: it offers the gazetteer both readings and
keeps whichever scores better, so EAST BEAVER CREEK matches
a street of that name exactly rather than a coin-toss between
East and West Beaver Creek.
A remark: municipality and postal city are not the same thing
Not a limit, but worth keeping in mind. A NAR row carries both a mailing municipality and a census subdivision, and the relationship between the two is complex in both directions — one postal city can span many municipalities, and one municipality can be served under several postal city names:
tbl(con, "MunAlias") |>
filter(NAME_FOLD %in% c("SCARBOROUGH", "TORONTO", "PRINCE RUPERT")) |>
count(NAME_FOLD, name = "csds_named") |>
arrange(csds_named) |>
collect()
#> # A tibble: 3 × 2
#> NAME_FOLD csds_named
#> <chr> <dbl>
#> 1 SCARBOROUGH 1
#> 2 TORONTO 5
#> 3 PRINCE RUPERT 47SCARBOROUGH names exactly one census subdivision —
Toronto, 234,568 addresses — a name that stopped being a municipality in
1998 and is still a perfectly good mailing address.
PRINCE RUPERT as a postal city reaches 47 of them.
MunAlias is the mapping between the two, built at
import, and both normalize_address() and
known$CSD_NAME resolve through it rather than comparing
strings. It is honest as a candidate set and dishonest as an assignment:
measured on 55,833 rows that carry both a postal city and a census
subdivision label, the alias set contains the right municipality every
single time — and its largest member alone is the right one 75.5% of the
time.
What this package deliberately does not do
- It does not repair NAR. No row is rewritten on import, and no tier reads the consistency analysis. A flagged row is a candidate for inspection, not a correction to apply.
-
It does not fold NAR’s positional error into
uncertainty_m. That error is not consistently knowable and inventing a number for it would corrupt the one quantity in the column that is measured. If a calibration ever becomes available it belongs in a separate column. -
It does not merge companion sources into
Addresses. Kept apart, RQA and the road network file stay measurable against NAR — which is where every figure quoted in this vignette came from. - It does not treat “the source answered” as “the address matched”. The BC geocoder, the geolocator and Quebec’s locator always return something; the tiers re-parse and score what comes back rather than trusting the response.
DBI::dbDisconnect(con)Where the measurements live
Every figure quoted above comes from a note that ships with the package, each of which records how it was measured and what it does not settle:
file.show(system.file("notes", "nar-consistency.md", package = "cangeocode"))
file.show(system.file("notes", "quebec-addresses.md", package = "cangeocode"))
file.show(system.file("notes", "road-network-file.md", package = "cangeocode"))
file.show(system.file("notes", "geocoding-status.md", package = "cangeocode"))
file.show(system.file("notes", "nova-scotia-pvsc.md", package = "cangeocode"))data-raw/probe_consistency.R reproduces the misplacement
analysis in about six minutes, data-raw/probe_rnf.R the
road network measurements, and data-raw/probe_pvsc.R the
Nova Scotia comparison — including the provenance test behind the table
above, which is its stage 0.
