8  Road Infrastructure as a Share of Municipal Area: France

8.1 Introduction

Road density is a common proxy for infrastructure in historical persistence research. A municipality with more roads relative to its area is better connected — and that connectivity may reflect centuries of investment or geographic advantage.

This notebook computes the share of each French municipality’s area covered by roads. The approach is:

  1. Draw a 10-metre buffer around each road segment (approximating its physical footprint).
  2. Intersect those buffers with municipality boundaries.
  3. Sum the buffered road area within each municipality and divide by total municipal area.

The result is a variable between 0 and 1 that can be merged into any France-level dataset for use as a control or outcome.

Data sources:

  • Road network: IGN ROUTE500 (France, 2021) — downloaded directly from IGN.
  • Municipality boundaries: giscoR LAU (Local Administrative Units) boundaries for France.

8.2 Step 1: Load packages

library(sf)
Linking to GEOS 3.10.2, GDAL 3.4.1, PROJ 8.2.1; sf_use_s2() is TRUE
library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.6.0
✔ ggplot2   3.5.2     ✔ tibble    3.3.0
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.2.0     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(archive)    # for extracting .7z files
library(geodata)    # for downloading GADM boundaries
Loading required package: terra
terra 1.8.80

Attaching package: 'terra'

The following object is masked from 'package:tidyr':

    extract

8.3 Step 2: Set up a temporary directory

We download raw data into a temporary directory so nothing is written permanently to the project folder.

tmp <- tempdir()

8.4 Step 3: Download and extract the IGN roads data

The IGN distributes ROUTE500 as a .7z archive. We download it with download.file() and extract it using the archive package, which supports the 7z format.

This file is large (~200 MB) so the download may take a few minutes.

roads_url  <- "https://data.geopf.fr/telechargement/download/ROUTE500/ROUTE500_3-0__SHP_LAMB93_FXX_2021-11-03/ROUTE500_3-0__SHP_LAMB93_FXX_2021-11-03.7z"
roads_7z   <- file.path(tmp, "ROUTE500.7z")

download.file(roads_url, destfile = roads_7z, mode = "wb")
archive::archive_extract(roads_7z, dir = tmp)
⠙ 13 extracted | 1.2 GB (477 MB/s) | 2.5s
⠹ 55 extracted | 1.4 GB (250 MB/s) | 5.5s
⠸ 60 extracted | 1.5 GB (179 MB/s) | 8.5s
⠼ 62 extracted | 1.7 GB (149 MB/s) | 11.5s

8.5 Step 4: Import the road network

The shapefile we need is TRONCON_ROUTE.shp — each row is one road segment. We filter out segments without a route number (NUM_ROUTE), which removes non-classified tracks. To keep memory use manageable we work with the first 100,000 segments.

shp_path <- file.path(
  tmp,
  "ROUTE500_3-0__SHP_LAMB93_FXX_2021-11-03",
  "ROUTE500", "1_DONNEES_LIVRAISON_2022-01-00175",
  "R500_3-0_SHP_LAMB93_FXX-ED211", "RESEAU_ROUTIER",
  "TRONCON_ROUTE.shp"
)

france_routes <- st_read(shp_path) |>
  filter(!is.na(NUM_ROUTE))
Reading layer `TRONCON_ROUTE' from data source 
  `/tmp/RtmpFrnFE7/ROUTE500_3-0__SHP_LAMB93_FXX_2021-11-03/ROUTE500/1_DONNEES_LIVRAISON_2022-01-00175/R500_3-0_SHP_LAMB93_FXX-ED211/RESEAU_ROUTIER/TRONCON_ROUTE.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 1302758 features and 12 fields
Geometry type: LINESTRING
Dimension:     XY
Bounding box:  xmin: 99873.3 ymin: 6050102 xmax: 1241907 ymax: 7110409
Projected CRS: RGF93 v1 / Lambert-93
france_routes <- france_routes[1:100000, ]

ggplot(france_routes) + geom_sf()

# Save the CRS — we will reuse it later
crs <- st_crs(france_routes)

8.6 Step 5: Download municipality boundaries from giscoR

giscoR::gisco_get_lau() downloads LAU (commune-level) boundaries from GISCO. We reproject to match the roads CRS and convert the pre-computed area from km² to m² so it is consistent with st_area() output.

library(giscoR)
france_communes <- giscoR::gisco_get_lau(country="France") |>
  st_transform(crs) |>
  mutate(total_area = AREA_KM2 * 1e6)  # convert km² → m² to match st_area() output

8.7 Step 6: Buffer the roads

st_buffer() requires a projected CRS in metres to produce a meaningful buffer distance. We:

  1. Temporarily reproject the roads to UTM zone 30 (metres).
  2. Draw a 10-metre buffer around each segment — approximating its physical road surface.
  3. Reproject the buffers back to the original CRS so they align with the communes.
france_routes <- st_transform(
  france_routes,
  "+proj=utm +zone=30 +datum=WGS84 +units=m +no_defs"
)

roads_buffer <- st_buffer(france_routes, 10)
roads_buffer <- roads_buffer |> st_transform(crs)

8.8 Step 7: Intersect road buffers with communes

st_intersection() clips each road buffer to the commune it falls in. The result is a data frame where each row is the piece of a road buffer that lies within a specific commune. We then compute the area of each piece.

intersected       <- st_intersection(roads_buffer, france_communes)
Warning: attribute variables are assumed to be spatially constant throughout
all geometries
intersected$area  <- as.numeric(st_area(intersected))

8.9 Step 8: Aggregate road area by commune

We sum the buffered road area within each commune (GISCO_ID is the unique commune identifier in the giscoR LAU dataset), then join the totals back onto the commune data frame.

The road surface share (sufrace) is then capped at 1 (in case rounding or overlapping buffers push it slightly above) and set to 0 for communes with no roads in this subset.

road_area_by_commune <- aggregate(
  intersected$area,
  by  = list(intersected$GISCO_ID),
  FUN = sum
)

france_communes <- france_communes |>
  left_join(road_area_by_commune, by = c("GISCO_ID" = "Group.1")) |>
  mutate(sufrace = as.numeric(x) / total_area) |>
  mutate(sufrace = case_when(
    sufrace > 1  ~ 1,
    is.na(sufrace) ~ 0,
    TRUE           ~ sufrace
  ))

8.10 Step 9: Export

We save the enriched commune dataset as a shapefile. Note that this file only reflects the first 100,000 road segments. To get complete coverage, run the same workflow on the full road network and sum the sufrace columns across all batches.

write_sf(france_communes, file.path(tmp, "france_communes_first_100000.shp"))

8.11 Step 10: Map the result

We map the log-transformed road surface share. log(1 + sufrace) compresses the right tail so that the colour scale is not dominated by a few highly urbanised communes.

france_communes |>
  filter(!grepl("^97|^98", LAU_ID)) |> # Keep european France
  ggplot(aes(fill = log(1 + sufrace))) +
  geom_sf(lwd = 0.0001) +
  scale_fill_viridis_c() +
  labs(fill = "log(1 + road share)")