Skip to contents

cangeocode is designed to provide open geocoding, reverse geocoding, and address normalization for Canadian addresses built on data under a permissive license. At the core sits Statistics Canada’s National Address Repository (NAR), a national list of structured civic addresses with coordinates. While a solid base, the NAR on it’s own can’t provide robust geocoding. Some records in the NAR lack spatial coordinates, some are sparially mis-located, and a range of addresses are missing entirely. cangeocode provides a multi-tiered approach to geocoding that uses NAR as the base, and adds additional sources to fill in the gaps.

Two important additions are Quebec’s Répertoire québécois des adresses which fills some impotant coverage gaps of the NAR, and Statistics Canada’s Road Network File, which is what reaches streets NAR does not carry.

The data is imported into a local DuckDB database for fast, offline access. The package provides a set of functions to query the database and geocode addresses, as well as a set of functions to normalize addresses and parse them into their components.

Everything runs on your own machine: no API keys, no rate limits, and no address ever leaves it. The exceptions are opt-in — geocode(method = c(..., "bc")) calls the Province of British Columbia’s public geocoder, geocode(method = c(..., "qc")) calls Quebec’s, geocode(method = c(..., "nrcan")) calls NRCan’s national one, and osm_geocode() calls the OpenStreetMap geocoder NRCan hosts. Nothing contacts any of them unless you ask for it.

What you may do with the results

The package is MIT, but the coordinates it hands back are not the package’s to licence — they belong to the sources it draws on. A default geocode() call mixes NAR (Statistics Canada Open Licence), Quebec’s register (CC-BY 4.0) and the road network file (Statistics Canada Open Licence); adding "bc", "qc" or "nrcan" brings in the Open Government Licence of British Columbia or of Canada. Every one of those is an attribution licence, and they compose: you may use, publish and sell work built on geocode() output, commercially included, provided each source you actually drew on is credited. Nothing else is asked of you — no share-alike, no fee, no key. match_method tells you which tier answered each row, so you can see which credits you owe; the CC-BY one is returned ready-made by rqa_attribution().

The one exception is osm_geocode(), whose OpenStreetMap data is ODbL — attribution plus share-alike, with the obligation attaching to a derived database. That is why it is not a geocode() tier: no default call can quietly change what you may do with your whole result table.

Installation

remotes::install_github("mountainMath/cangeocode")

Setup

Before the first call, tell the package where to keep its database by setting NAR_CACHE_PATH. There is no default, and every entry point stops with an error if it is unset. Add it to your ~/.Renviron:

NAR_CACHE_PATH=~/data/nar

The first call then downloads the NAR release from StatCan and imports it. Expect this to take a while and to use real disk — 1.7 GB over the wire (the package raises R’s timeout to 20 minutes, because the StatCan connection can be slow) and roughly 5 GB on disk for the 2026-06 release. Every call after that opens the existing database instantly. In an interactive session the first call asks whether you want all of it.

Just one province

The StatCan release is split by province and the server honours range requests, so you can download only the provinces you need:

con <- nar_connection(provinces = "PE")

That is 10 MB and about 40 seconds for a working Prince Edward Island geocoder — British Columbia is 192 MB, Ontario 552 MB. The addresses are the same NAR rows either way, so a partial database geocodes its own provinces exactly as well as a national one does; it simply holds nothing outside them, which geocode() reports as not_covered rather than as a failure to match.

Coverage is recorded in the database and checked before anything is downloaded, so asking for a province you already have downloads nothing, and asking for one you do not adds just that province rather than rebuilding:

nar_provinces(con)
#> [1] "PE"
con <- nar_connection(provinces = c("PE", "NB"))   # fetches NB only

Documentation

Full reference and articles: https://mountainmath.github.io/cangeocode/

The data sources (vignette("data-sources")) is the overview: how the seven sources relate, what each layer of the tier chain is worth, which of them can be trusted to check another, and why one of them is not a tier. Below it sits one vignette per source, each covering what it adds to the package, the licence it comes with, and what to watch out for:

vignette source role licence
source-nar National Address Repository the default tier that does most of the heavy lifting StatCan Open Licence
source-rqa Répertoire québécois des adresses offline "rqa" tier, via rqa_import() CC-BY 4.0
source-rnf Statistics Canada Road Network File offline "rnf" tier, via rnf_import() StatCan Open Licence
source-bc BC Address Geocoder online tier, and bc_validate() OGL – British Columbia
source-nrcan NRCan national geolocator online tier OGL – Canada
source-qc Quebec MRNF geocoder online tier, qc_validate(), qc_reverse_geocode() CC-BY 4.0
source-osm NRCan-hosted Nominatim bound as osm_geocode(), deliberately not a tier ODbL 1.0

The last column is why the last row is not a tier: the first six are attribution licences and compose with each other, ODbL adds share-alike and does not. See what you may do with the results.

And the changelog.

Longer notes ship with the package and say what does not work yet, with the measurements behind each claim. system.file("notes", package = "cangeocode") lists them; the two to start with are geocoding-status.md and address-normalization-status.md. The rest go one level down: road-network-file.md and quebec-addresses.md measure the two sources that can be imported beside NAR, nrcan-geolocator.md reads the national geolocator from its own source, deepparse.md measures the address parser against a purpose-built neural tagger and is where the case for a trained model is made and rejected, and nar-consistency.md finds NAR’s own misplaced addresses using nothing but NAR.

Geocoding

Turn address strings into coordinates:

library(cangeocode)

geocode(c("1055 W Georgia St, Vancouver BC",
          "100 Queen St W, Toronto, ON",
          "1225 rue Notre-Dame Ouest, Montreal, QC",
          "9999 Jasper Ave, Edmonton, AB"))
#>                                     input     match_method uncertainty_m
#> 1         1055 W Georgia St, Vancouver BC     nar_building           0.0
#> 2             100 Queen St W, Toronto, ON     nar_building           0.0
#> 3 1225 rue Notre-Dame Ouest, Montreal, QC     nar_building           0.0
#> 4           9999 Jasper Ave, Edmonton, AB nar_interpolated          21.3
#>          lon      lat
#> 1 -123.12141 49.28529
#> 2  -79.38250 43.65150
#> 3  -73.56459 45.49367
#> 4 -113.49028 53.54101

One row per input, in input order, carrying the parsed address components alongside the result. geometry = TRUE returns an sf object instead of lon/lat.

Every result says how it was found and what that cost. match_method is nar_building when the civic number is in NAR with its own point, nar_blockface when only a street-segment centroid is available, nar_interpolated when the number was placed between the flanking civics, and none when nothing resolved. uncertainty_m is the 90th-percentile error the package adds relative to NAR’s own point — 0 for a building match, 176 m for a blockface one, half the flanking span for an interpolated one. It says nothing about NAR’s own error, which is a separate quantity — the notes linked above measure that separately, for one province.

n_matches counts points; n_records counts addresses. The two come apart whenever a building has units, since NAR files each unit as its own address at the one coordinate — 6093 Iona Dr, Vancouver is one point and 33 records. That is not an error, and 47% of NAR’s placed addresses share their coordinate with another. It matters when the collapsed records disagree about something you were using, which today means the postal code.

Naming the unit narrows the records. 49321 Range Road 72 is 19 records; 49321 Range Road 72, Unit 9 is one, with a postal code the wider set had to decline. The narrowing narrows or it does nothing — a unit NAR does not carry at that civic number is dropped rather than enforced, so an address is never made unplaceable by a unit label. That matters more than it sounds: across 5,000 Corporations Canada filings, 27.5% of the units supplied are not in NAR at the civic number they were written against.

geocode_matches() opens those records up — one row each, ranked so the first is the one geocode() answered with:

geocode_matches("49321 Range Road 72") |>
  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           T7A2A2
#> 2           2            7 BRAZEAU COUNTY           T7A2A2
#> 3           3           25 DRAYTON VALLEY           T7A1R9
#> ...                                                       

It is the exact NAR tier only: an address that reached a coordinate by interpolation or from an online service resolved to no record, so it has no matches to show.

Two postal codes, and they are not two attempts at the same thing. POSTAL_CODE is what the input string said — usually NA, since most addresses are typed without one — and match_postal_code is what the matched record carries. Only the tiers that resolve to a record fill the second one, and it is left empty rather than guessed: an interpolated point sits between two addresses that need not share a postal code, and a building whose units span two reports neither unless the input said which unit it meant.

Interpolation is deliberately conservative: same side of the street only, and it refuses to extrapolate past the last known civic rather than guessing.

Batch rather than loop — the street-name join costs about the same for 5 addresses as for 200, and every row in a call shares it.

Choosing the methods

method names the tiers to try and the order to try them in. Each tier only sees the rows the ones before it left without a position, so the order is the priority:

geocode(addresses, method = "nar")                      # NAR records only
geocode(addresses, method = c("nar", "nar_interpolate")) # the default
geocode(addresses, method = c("nar", "nar_interpolate", "rnf"))
geocode(addresses, method = c("nar", "nar_interpolate", "bc"))
geocode(addresses, method = c("nar", "nar_interpolate", "nrcan"))
geocode(addresses, method = c("bc", "nar"))             # prefer BC where it answers

"nar" looks the civic number up directly and "nar_interpolate" places the ones NAR does not carry; "rqa" reads Quebec’s own address register and "rnf" the national road network file, both if you imported them; "bc" asks the Province of BC’s geocoder and "nrcan" NRCan’s national one. The default is the offline pair; nothing reaches the network unless "bc", "nrcan" or "qc" is named, and those belong last in the list.

known is what you already know about the address, in the same column names the output uses: CIVIC_NO, STREET_NAME, STREET_TYPE, STREET_DIR, APT_NO_LABEL, CIVIC_NO_SUFFIX, MUN_NAME, CSD_NAME, PROV_ABVN, POSTAL_CODE. Every component is authoritative: it overrides whatever the address string said, it constrains the search, and it lands on the returned row too. So does within.

# A bounding box, an sf polygon, or an st_bbox all work.
geocode(addresses, known = list(PROV_ABVN = "BC"),
        within = sf::st_bbox(vancouver))

The two municipality components are two different questions. MUN_NAME is the mailing city, compared straight at the name on the envelope. CSD_NAME is the census subdivision, resolved through NAR’s alias set, so CSD_NAME = "Toronto" finds the addresses NAR files under SCARBOROUGH, ETOBICOKE and NORTH YORK and MUN_NAME = "Toronto" does not. Supply both to narrow to one community inside an amalgamated city.

Address normalization

normalize_address() turns free text into the fields an address is actually made of. It is what geocode() parses with, and it is useful on its own wherever addresses have to be compared rather than mapped.

normalize_address("Suite 800 - 666 Burrard Street, Vancouver, B.C. V6C 2X8",
                  con = con)
#>   APT_NO_LABEL CIVIC_NO STREET_NAME STREET_TYPE  MUN_NAME PROV_ABVN POSTAL_CODE
#> 1          800      666     Burrard          ST VANCOUVER        BC      V6C2X8
#>     pattern confidence parse_source
#> 1 unit_civic          1    gazetteer

One row per input, in input order. Passing con additionally resolves the parse against NAR’s street gazetteer, which corrects misspellings no rule could — 29 HPCKING AVE, SAULT STE. MARIE comes back as Hocking, the one real street in that city close enough to be meant — and parse_source says which rows cleared it.

normalize_address() takes the same known list as geocode(), which is how an address that was never one string — a roll with the community and the postal code in columns of their own — reaches the parser with its structure intact.

Canonical forms are province-conditioned. NAR writes AVE in Ontario and AV in Quebec, BLVD against BOUL, W against O, so known$PROV_ABVN is not a hint but a choice of vocabulary. Pass it if your data has it.

address_pattern() sorts a string into one of twelve structural shapes and needs no database at all. Two of them exist to say this will never resolve: po_box and rural_route are delivery instructions, and NAR contains neither. Separating those out first keeps them from sitting in the failure pile looking like parser bugs.

address_key() puts the components back together as a single string that two spellings of the same address share, which is what matching two lists needs:

address_key(c("1055 West Georgia Street, Vancouver, British Columbia",
              "Suite 1500 - 1055 W Georgia St, Vancouver, B.C. V6E 4N7",
              "1055 w georgia st vancouver bc"), con = con)
#> [1] "BC|VANCOUVER|GEORGIA|ST|W|1055|" "BC|VANCOUVER|GEORGIA|ST|W|1055|"
#> [3] "BC|VANCOUVER|GEORGIA|ST|W|1055|"

Case, accents and the punctuation NAR and the parser disagree on are all folded out, the unit is left out unless you ask for it, and a row with no street name keys to NA rather than to an empty string so unparseable rows cannot all join to each other. format_address() is the readable counterpart, for writing a cleaned column back out.

Measured on 5,000 real filings nobody cleaned, the parser extracts a civic number and street name from 98.9% and resolves 88.8% to an address NAR actually holds. In Quebec, running rqa_import() adds a second gazetteer pass over the rows NAR could not settle, and gives the answer a second register to be confirmed against: 77.5% of Quebec filings resolve to a NAR address, 83.0% to one in NAR or Quebec’s own register. vignette("address-normalization") has the rest of the numbers and the known limits.

Canada-wide: the NRCan geolocator

nrcan_geocode() binds NRCan’s geolocator. It is national, needs no API key and no local database, which makes "nrcan" the one tier that answers before a NAR release has been downloaded — and the one that covers provinces a single-province import does not hold.

geocode(addresses, method = c("nar", "nar_interpolate", "nrcan"))

Two things to know before relying on it. The service always answers, and without a score to disbelieve: asked for a Montreal street it will happily return the same street name 500 km away. cangeocode therefore re-parses the address it gets back and requires it to agree, component by component, with the one that was sent; roughly half of what is asked is rejected on that basis, and the rejected rows keep a nrcan_reject column saying why. And as a fallback for NAR’s own gaps it is worth much less than BC’s geocoder — it recovers about 8% of what NAR leaves unplaced, because the addresses NAR lacks are largely the ones no national compilation has.

It does not do reverse geocoding; reverse_geocode() is NAR-backed and local.

British Columbia: a second geocoder

bc_geocode() binds the Province of BC’s Address Geocoder. It covers BC only, needs no API key, and is useful in two ways.

As a last-resort tier, for the BC addresses NAR cannot place:

geocode(addresses, method = c("nar", "nar_interpolate", "bc"))

On a sample of 600 BC addresses the NAR pathway placed 524 and gave up on 76; the fallback resolved 75 of those, 31 of them at address level.

As validation, since it is a second source of position — and the more reliable of the two where they differ, being a parcel-level provincial authority rather than a national compilation:

g <- geocode(c("525 Superior St, Victoria, BC", "800 Robson St, Vancouver, BC"))
bc_validate(g)
#>                           input     match_method bc_match_method bc_dist_m
#> 1 525 Superior St, Victoria, BC     nar_building        bc_civic       6.9
#> 2  800 Robson St, Vancouver, BC nar_interpolated        bc_civic     104.1

A response from this service is not a match. It always answers — feed it "1234 Nonexistentzzz Rd, Victoria, BC" and it returns the centre of Victoria with a score of 48 rather than an error. So match_method is derived from the service’s own precision vocabulary, and min_score (default 60) rejects what it scored badly. Rejected rows keep their bc_score and bc_faults, so you can see what was thrown away and why.

Like the geolocator, this is a path on which addresses leave your machine. Requests are throttled to be a good citizen of a free public service; register for an API key and pass it as api_key for anything large.

The road network file: streets NAR does not have

The largest single thing geocode() fails on is an address whose street is not in NAR at all — a new subdivision, a street built since the last release. Statistics Canada’s Road Network File carries every street in the country with an address range on each side of each segment, and rnf_import() loads it into the same DuckDB file, in tables of its own:

rnf_import()                                   # ~340 MB download, once
geocode(addresses, method = c("nar", "nar_interpolate", "rnf"))

The "rnf" tier places the civic number along the segment whose range contains it, offset to the correct side of the centreline. It is offline, national, and not in the default method, because the tables only exist if you ran the import. It belongs after interpolation: a point placed between two known civics is better than one placed along a range.

On the same 5,000 business filings the rest of this README measures:

placed
c("nar", "nar_interpolate") 92.4%
c("nar", "nar_interpolate", "rnf") 94.3%

That is 93 of the 379 addresses the offline pair gives up on — a quarter of the residual, and the largest recovery any tier here offers. Spot-read, they are Ryan Reynolds Way, Celebration Dr, Grosbeak Trl: streets that exist and are too new for NAR.

It refuses rather than guessing when the street is ambiguous. If more than one segment of that name in that municipality has a range containing the number, match_method is rnf_ambiguous, n_matches says how many were in contention, and no coordinate is returned. Ambiguity is where this file’s gross errors live — those rows run a 90th-percentile 1.7 km against 108 m for the rest — and refusing costs 7 rows in 5,000.

Treat it as the coarse tier it is. uncertainty_m is max(95, 0.35 × len_m), never 0, and the addresses NAR cannot place are measurably harder than the ones it can: checked against the filer’s own postal code, which the tier never reads, recovered rows sit a median 149 m from their urban FSA centroid against 60 m for rows NAR also placed. Filter on uncertainty_m rather than treating every row alike.

The ranges themselves carry no provenance flag — an observed range and an imputed one are the same bytes — so everything above rests on measuring the file against NAR rather than on the file’s own word. inst/notes/road-network-file.md is that measurement.

Quebec: importing the address register

Quebec’s own Répertoire québécois des adresses (RQA) carries about 750,000 certified addresses more than NAR’s Quebec extract does. rqa_import() loads it into the same DuckDB file, in tables of its own rather than into Addresses:

rqa_import()                                   # ~780 MB download, once
geocode(addresses, method = c("nar", "rqa", "nar_interpolate"))

The "rqa" tier is offline, Quebec-only, and belongs before interpolation — a register point, however coarse, beats a point interpolated between two neighbours. It is not in the default method, because the tables only exist if you ran the import.

On 4,000 Quebec business filings:

placed placed on a register point
c("nar", "nar_interpolate") 88.5% 82.7%
c("nar", "rqa", "nar_interpolate") 90.1% 89.1%

The second column is the point. 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 instead, a median of 26 m away. It costs nothing measurable, since it only ever sees the rows NAR left behind.

They stay separate on purpose. Keeping the two registers separately readable is the only reason any of the coverage above is measurable at all, and the rows RQA adds are positionally weaker than what NAR carries — 20.3% of them are building points against 26.9% register-wide, and 30.0% are flagged Incertaine. So match_method reports the register’s own class (rqa_building, rqa_geocoded, rqa_uncertain, rqa_lot) and uncertainty_m is filled in only for rqa_building, where 0 means the same thing it means for NAR. Every imported row carries IN_NAR, and RqaStreets counts the addresses per street that NAR has no row for.

The register is CC-BY 4.0, where everything else here is an open government licence. Anything published from these points has to carry the attribution, which rqa_attribution() returns.

See vignette("geocoding") and inst/notes/quebec-addresses.md for what the gap is made of and what importing it does and does not buy.

Quebec: a second geocoder, and reverse geocoding

qc_geocode() binds the Ministère des Ressources naturelles et des Forêts’ geocoder over the Répertoire québécois des adresses. It covers Quebec only, needs no API key, and batches up to 1000 addresses per request.

As a last-resort tier, for the Quebec addresses NAR cannot place:

geocode(addresses, method = c("nar", "nar_interpolate", "qc"))

On 600 Quebec addresses the NAR pathway placed 81.0%; the fallback took that to 83.3%, all of it at civic level.

It also does reverse geocoding, which none of the other online services here do:

qc_reverse_geocode(-73.5672, 45.5017)
#>                                    qc_address qc_postal  qc_city qc_dist_m
#> 1 706 Boulevard René-Lévesque Ouest, Montréal    H3H2S1 Montréal       2.8
#>         lon      lat
#> 1 -73.56718 45.50168

Treat qc_validate() as a fallback check, not an independent one. The service’s locators are named RQA_Adresse and RQA_Rue: it serves the Répertoire, which is also what NAR’s Quebec records are built from. Over 400 NAR Quebec addresses the median disagreement is 0.9 m — evidence that the two share an upstream, not that either is accurate.

A response from this service is not a match either, and its qc_score is not a quality ranking: street-level answers score higher than civic ones, and the correlation between score and positional error is nil. match_method comes from which locator answered, and the returned address is re-parsed and compared against what you asked for before the row is accepted. Rejected rows keep qc_locator, qc_score and qc_reject, so you can see what was thrown away and why.

OpenStreetMap: Canada’s own Nominatim

osm_geocode() binds the Nominatim instance Natural Resources Canada hosts on the Federal Geospatial Platform — not the volunteer-funded nominatim.openstreetmap.org, whose usage policy forbids bulk geocoding. It needs no API key and no local database.

osm_geocode("990 Bute St, Vancouver, BC")

It is not one of geocode()’s tiers, and the reason is the licence rather than the accuracy: OpenStreetMap data is ODbL, which carries attribution and share-alike obligations that attach to a derived database, where every other source in this package is under an attribution-only open licence. Folding a few ODbL rows into a result table by default would quietly change what you may do with the whole table, so this one is an explicit call and each row carries the service’s own osm_licence string.

The pleasant surprise is that it refuses. Where the other two services always answer, this one returns nothing for an address it does not have, and the street rather than a point when it has the street but not the number — so a result is far more likely to mean what it says. What is unmeasured is coverage: OpenStreetMap’s Canadian addresses are concentrated in cities with municipal open-data imports, and no national comparison has been run yet.

Reverse geocoding

Find the addresses closest to a coordinate:

reverse_geocode(c(-123.2, 49.25), match_radius = 100)
#> # A tibble: 16 × 32
#>    address                                        dist geom_source ...
#>  1 4176 W KING EDWARD AVE, VANCOUVER V6S1N3       23.5 building
#>  2 4172 W KING EDWARD AVE, VANCOUVER V6S1N3       27.4 building
#>  3 4182 W KING EDWARD AVE, VANCOUVER V6S1N3       32.3 building
#>  ...

Coordinates are longitude/latitude (EPSG:4326) by default; sf points and other CRSs work too. match_radius and dist are in metres. output picks between all matches, the components of the closest one, or a single formatted address string, and geometry = TRUE returns an sf object.

This is local and NAR-backed. qc_reverse_geocode() is the one online alternative here, and it covers Quebec only.

Querying the database directly

Every entry point takes a con argument, and none of them need it. geocode() and reverse_geocode() open a connection when con is not supplied and keep it open for the next call, so a loop pays for the database once rather than once per address:

addresses <- lapply(points, \(p) reverse_geocode(p, output = "address"))

open_nar() does that up front — worth it to name a release other than the latest, or a province subset, without repeating it at every call site — and close_nar() ends it. Neither is required, and an import that needs to write to the database closes the connection itself.

open_nar(version = "2025-12")
geocode("100 Queen St W, Toronto, ON")
close_nar()

A connection you open yourself is a plain DBI connection, so the full address table is open to dplyr — filtered, joined, and aggregated inside DuckDB:

library(dplyr)

con <- nar_connection()

tbl(con, "Addresses") |>
  filter(MAIL_MUN_NAME == "VANCOUVER", MAIL_STREET_NAME == "KINGSWAY") |>
  select(ADDR_GUID, CIVIC_NO, MAIL_POSTAL_CODE, geom) |>
  collect_nar()          # returns an sf object

Where NAR’s points come from

This applies to both directions of geocoding. Most NAR addresses carry a point for the building itself. About 7% do not, and for those the package falls back to the blockface point — the centre of one side of a street between two intersections — which is shared by every address on that stretch and is a good deal coarser. Results say which was used in the geom_source column — and forward geocoding in match_method — so filter on it when a distance needs to mean what it appears to mean. A further ~65,000 addresses have no coordinates at all, which geocode() reports as nar_no_geometry rather than as a failure to find the address.

Note that even a building point is a representative point: the StatCan user guide allows it to be the road access point or the driveway, and publishes no accuracy figure. That is why uncertainty_m = 0 means “this package added nothing”, not “this point is exact”.

Data source

NAR is published by Statistics Canada under the Statistics Canada Open Licence. The BC Address Geocoder is a service of the Province of British Columbia and carries its own terms, which its responses link to. The Répertoire québécois des adresses, which rqa_import() loads and qc_geocode() queries, is published by the Ministère des Ressources naturelles et des Forêts under CC-BY 4.0 — a different licence from the rest, and one that requires attribution; rqa_attribution() returns it. This package is not affiliated with or endorsed by any of them.