Portuguese data usually identifies places in one of three ways: by official code (the 4-digit DTMN for a municipality, the 6-digit DTMNFR for a freguesia), by name, in whatever spelling the source uses, or by NUTS code. This guide shows how to get each of them onto a map, and how to deal with the usual problems: repeated names, lost leading zeros and spelling variants.
Join by code when available
Codes are unambiguous, so use them whenever possible:
df <- data.frame(dtmn = c("1106", "1312"), value = c(20, 10))
m <- get_map("municipio", data = df, by = "dtmn")
#> get_map: 306 boundaries have no data row (NA fill).Excel often turns "0115" into 115. geopt
restores the leading zeros when it joins. To fix a column directly:
pt_pad_code(c(115, 1106), "municipio")
#> [1] "0115" "1106"Join by name
Names are matched by pt_match(), which ignores accents,
case and punctuation, and reads the abbreviations S.,
Sta. and Sto. as São, Santa and Santo:
df <- data.frame(concelho = c("porto", "CÂMARA DE LOBOS", "S. João da Madeira"),
value = c(1, 2, 3))
pt_match(df$concelho)
#> [1] "1312" "3102" "0116"
#> attr(,"report")
#> input matched_name code method dist
#> 1 porto Porto 1312 exact NA
#> 2 CÂMARA DE LOBOS Câmara de Lobos 3102 exact NA
#> 3 S. João da Madeira São João da Madeira 0116 exact NARepeated names
Some names are shared: two municipalities are called
Lagoa (one in Faro district, one on São Miguel), and
196 freguesia names are used by more than one freguesia. A name that
fits more than one unit returns NA with a warning. geopt
never guesses:
pt_match("Lagoa")
#> Warning: pt_match: 1 ambiguous name(s) returned NA. Pass `context` or use codes.
#> 'Lagoa' (2 candidates: Faro, Ilha de São Miguel)
#> [1] NA
#> attr(,"report")
#> input matched_name code method dist
#> 1 Lagoa <NA> <NA> ambiguous NAThere are two ways to say which one is meant. Add the region in
brackets after the name, as INE’s files often do, or pass a
context, the unit it belongs to:
pt_match(c("Lagoa (Faro)", "Lagoa (Açores)", "Lagoa (R.A.A.)"), quiet = TRUE)
#> [1] "0806" "4201" "4201"
#> attr(,"report")
#> input matched_name code method dist
#> 1 Lagoa (Faro) Lagoa 0806 exact NA
#> 2 Lagoa (Açores) Lagoa 4201 exact NA
#> 3 Lagoa (R.A.A.) Lagoa 4201 exact NA
pt_match("Lagoa", context = "Faro", quiet = TRUE)
#> [1] "0806"
#> attr(,"report")
#> input matched_name code method dist
#> 1 Lagoa Lagoa 0806 exact NAAt freguesia level, repeated names are common, so always give the municipality as context. No municipality has two freguesias with the same name, so this always settles it:
df <- data.frame(freg = c("Pinheiro", "Pinheiro"),
mun = c("Guimarães", "Castro Daire"))
pt_match(df$freg, level = "freguesia", context = df$mun, quiet = TRUE)
#> [1] "030836" "180319"
#> attr(,"report")
#> input matched_name code method dist
#> 1 Pinheiro Pinheiro 030836 exact NA
#> 2 Pinheiro Pinheiro 180319 exact NANames from older data
In 2013, about half of all freguesias were merged into uniões de freguesias, and in 2025 some of them were split again. Names from before 2013 match the freguesia that covers the same place today, so a file from an older census joins without being converted first:
pt_match(c("Sé", "Maximinos"), level = "freguesia",
context = "Braga", quiet = TRUE)
#> [1] "030364" "030364"
#> attr(,"report")
#> input matched_name code method dist
#> 1 Sé Braga (Sé) 030364 historic NA
#> 2 Maximinos Braga (Maximinos) 030364 historic NABoth are now part of
União das freguesias de Braga (Maximinos, Sé e Cividade).
The report marks these matches as "historic", which shows
how much of a join relied on old names. A name that is still in use
always takes priority over an old one, so this never changes what a
current name matches. Pass historic = FALSE to accept only
current names.
To get the code each freguesia had before 2013 instead, pass
admin_vintage = "2011" (the map the 2011 census used). Each
old freguesia then keeps its own code, rather than sharing the code of
the união it joined:
pt_match(c("Sé", "Maximinos"), level = "freguesia", context = "Braga",
admin_vintage = "2011", quiet = TRUE)
#> [1] "030352" "030324"
#> attr(,"report")
#> input matched_name code method dist
#> 1 Sé Braga (Sé) 030352 exact NA
#> 2 Maximinos Braga (Maximinos) 030324 exact NATo convert the values in a dataset between these versions of
the map, see pt_recode() and
vignette("vintages").
Spelling variants and typos
If a source consistently spells a name its own way, pass
aliases:
pt_match("VN Gaia", aliases = c("VN Gaia" = "Vila Nova de Gaia"), quiet = TRUE)
#> [1] "1317"
#> attr(,"report")
#> input matched_name code method dist
#> 1 VN Gaia Vila Nova de Gaia 1317 alias NAFor typos, turn on fuzzy matching (this needs the stringdist package). It only runs on names that didn’t match exactly, only accepts a candidate that is clearly the closest, and reports every match it makes:
pt_match("Guimaraess", fuzzy = TRUE)
#> pt_match: 1 fuzzy match(es):
#> 'Guimaraess' -> 'Guimarães' (dist 1)
#> [1] "0308"
#> attr(,"report")
#> input matched_name code method dist
#> 1 Guimaraess Guimarães 0308 fuzzy 1Checking what matched
Every result from pt_match() and pt_join()
carries a report. Check it before trusting a join:
res <- pt_match(c("Porto", "Lagoa", "Atlantis"), quiet = TRUE)
pt_match_report(res)
#> input matched_name code method dist
#> 1 Porto Porto 1312 exact NA
#> 2 Lagoa <NA> <NA> ambiguous NA
#> 3 Atlantis <NA> <NA> unmatched NAThe method column says how each input was matched, or
why it wasn’t:
method |
Meaning |
|---|---|
code |
the input was an official code |
exact |
the name matched, once accents, case and punctuation were ignored |
loose |
the name matched once small words like “da” or “dos” were ignored |
historic |
the name is from an older map, such as one from before 2013 |
alias |
the name matched through aliases
|
fuzzy |
a close spelling matched (only with fuzzy = TRUE) |
ambiguous |
more than one unit has this name: add a context |
context_unrecognized |
the given context isn’t a unit geopt knows |
unmatched |
no unit has this name |
empty |
the input was empty or NA
|
Three ways to join
get_map(data =, by =) joins straight
onto the boundaries, for maps:
pt_join() adds to the data frame the
codes and the regions each unit belongs to, for work that doesn’t need a
map. It only adds columns that have a single value for each unit at the
level joined on. Some municipalities are split between several ULS, for
example, so a join by municipality gets uls_municipio
rather than uls (see
vignette("health-geographies")):
df <- data.frame(concelho = c("Porto", "Águeda"), value = c(1, 2))
out <- pt_join(df, by = c(municipio = "concelho"))
#> pt_join: 2/2 row(s) matched at municipio level.
names(out)
#> [1] "concelho" "value" "dtmn" "municipio"
#> [5] "dt" "distrito_ilha" "nuts1_cod" "nuts1"
#> [9] "nuts2_cod" "nuts2" "nuts3_cod" "nuts3"
#> [13] "reg_saude" "uls_municipio" "reg_saude_2022" "aces_municipio"Like pt_match(), pt_join() takes
admin_vintage and nuts_vintage. INE publishes
freguesias on the 2013 map, so for its data
admin_vintage = "2013" (or "auto") keeps the
2013 codes and adds that map’s names, and nuts_vintage
picks the NUTS revision of the regions added:
ine <- data.frame(dtmnfr = "030882", value = 3) # a união split in 2025
pt_join(ine, by = "dtmnfr", admin_vintage = "2013")[, c("dtmnfr", "freguesia", "municipio")]
#> pt_join: 1/1 row(s) matched at freguesia level.
#> dtmnfr freguesia municipio
#> 1 030882 União das freguesias de Conde e Gandarela Guimarãespt_lookup() returns the table of codes
and regions itself, as a plain data frame, ready for any other kind of
join. Health columns are only added when health_vintage
says which version of the health map to use:
codes <- pt_lookup(health_vintage = "2024")
head(codes[, c("dtmnfr", "freguesia", "dtmn", "municipio", "nuts3_cod", "unit")])
#> dtmnfr freguesia dtmn municipio
#> 1 010103 Aguada de Cima 0101 Águeda
#> 2 010109 Fermentelos 0101 Águeda
#> 3 010112 Macinhata do Vouga 0101 Águeda
#> 4 010119 Valongo do Vouga 0101 Águeda
#> 5 010124 União das freguesias de Recardães e Espinhel 0101 Águeda
#> 6 010125 União das freguesias de Travassô e Óis da Ribeira 0101 Águeda
#> nuts3_cod unit
#> 1 PT191 ULS Região de Aveiro
#> 2 PT191 ULS Região de Aveiro
#> 3 PT191 ULS Região de Aveiro
#> 4 PT191 ULS Região de Aveiro
#> 5 PT191 ULS Região de Aveiro
#> 6 PT191 ULS Região de AveiroCleaning names directly
The name cleaning that pt_match() uses is available as
pt_normalize(), so the same rules can be applied to any
column before a manual join:
pt_normalize(c("S. Jorge", "SÃO JORGE", "Sao-Jorge"))
#> [1] "sao jorge" "sao jorge" "sao jorge"