I needed a way to add nodes to a network when using pgRouting but did not have the desire to do so in PostGIS, so I wrote a function which I can use in R before writing the layer to the database.

st_blend = function(node, network) {
  # blends node(s) into a network composed of linestrings
  # node: point(s)
  # network: network composed of linestrings
  # returns: network with added node(s)
  
  library(sf)
  library(lwgeom)
  library(dplyr)
  library(purrr)
  
  ncrs = st_crs(network)
  blade = st_sf(node) %>%
    mutate(geometry = st_geometry(.)) %>% # just in case geometry column isn't named geometry
    st_set_geometry("geometry") %>%
    mutate(
      nf = st_nearest_feature(., network),
      # nearest linestring
      nl = do.call(c, map2(
        geometry, nf,  ~ st_nearest_points(st_sfc(.x, crs = ncrs), network[.y, ])
      )),
      # nearest point to edge/linestring
      np = do.call(c, map2(
        nl, geometry, ~ st_cast(st_sfc(.x, crs = ncrs), "POINT") %>% .[. != st_sfc(.y, crs = ncrs)]
      )),
      # nearest point in nearest edge/linestring
      ns = do.call(c, map2(
        nf, np, ~ st_cast(st_geometry(network[.x, ]), "POINT") %>% .[st_nearest_feature(st_sfc(.y, crs = ncrs), .)]
      )),
      len = st_length(nl) %>% units::drop_units()
    ) %>%
    filter(np != ns) %>% # remove rows where nearest point is already in nearest edge/linestring
    st_set_geometry("nl") %>%
    select(nf, len) %>%
    suppressWarnings()
  
  blade = st_geometry(blade) %>%
    {
      (. - st_centroid(.)) * (blade$len * 2 / blade$len) + st_centroid(.) # ensure line is long enough to split edge
    } %>%
    st_set_crs(ncrs) %>%
    st_sf() %>%
    mutate(nf = blade$nf)
  
  st_split(network[blade$nf, ], blade) %>%
    st_collection_extract("LINESTRING") %>%
    rbind(network[-blade$nf, ])
}

rowwise from dplyr could have been used in place of the purrr functions here, though map2 ended up being faster.

library(sf)
library(dplyr)
library(tigris)
library(mapview)

addison = counties("vt", progress = F) %>% filter(COUNTYFP == "001") %>% st_transform(32145)

schools = read_sf(
  "https://opendata.arcgis.com/datasets/efa79839b0f841ac92c821a7b8afeda5_3.geojson"
) %>%
  st_transform(32145) %>%
  filter(lengths(st_intersects(., addison)) != 0)

streets = read_sf(
  "https://opendata.arcgis.com/datasets/1dee5cb935894f9abe1b8e7ccec1253e_39.geojson"
) %>%
  st_transform(32145) %>%
  st_intersection(st_geometry(addison)) %>%
  st_multipart_to_singleparts() %>% # need to make sure all features are linestrings  
  mutate(id = row_number())

blended = st_blend(schools, streets) %>%
  group_by(id) %>%
  mutate(cnt = n()) %>%
  ungroup

split_streets = filter(blended, cnt > 1) %>%
  mutate(color = ifelse(as.numeric(row.names(.)) %% 2 != 0, "split", "street"))

unsplit = filter(blended, cnt == 1)

mapview(
  select(split_streets, color),
  layer.name = "Split Streets",
  color = c("#F3D54E", "#69B3E7")
) +
  mapview(unsplit, layer.name = "Unsplit Streets", color = "#4C4B4C") +
  mapview(
    schools,
    layer.name = "Addison County Schools",
    col.regions = "#0D395F",
    color = "#000000",
    alpha = .2
  )

In this example, the linestrings nearest to the schools are either split or are left untouched if the nearest point from the school already exists as a vertex in the linestring. I found that sfneworks has a function called st_network_blend after writing this so I may use that in the future, though writing st_blend was still a fun exercise.

LS0tDQp0aXRsZTogInN0X2JsZW5kIg0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KSSBuZWVkZWQgYSB3YXkgdG8gYWRkIG5vZGVzIHRvIGEgbmV0d29yayB3aGVuIHVzaW5nIHBnUm91dGluZyBidXQgZGlkIG5vdCBoYXZlIHRoZSBkZXNpcmUgdG8gZG8gc28gaW4gUG9zdEdJUywgc28gSSB3cm90ZSAgYSBmdW5jdGlvbiB3aGljaCBJIGNhbiB1c2UgaW4gUiBiZWZvcmUgd3JpdGluZyB0aGUgbGF5ZXIgdG8gdGhlIGRhdGFiYXNlLiANCmBgYHtyIHN0X2JsZW5kfQ0Kc3RfYmxlbmQgPSBmdW5jdGlvbihub2RlLCBuZXR3b3JrKSB7DQogICMgYmxlbmRzIG5vZGUocykgaW50byBhIG5ldHdvcmsgY29tcG9zZWQgb2YgbGluZXN0cmluZ3MNCiAgIyBub2RlOiBwb2ludChzKQ0KICAjIG5ldHdvcms6IG5ldHdvcmsgY29tcG9zZWQgb2YgbGluZXN0cmluZ3MNCiAgIyByZXR1cm5zOiBuZXR3b3JrIHdpdGggYWRkZWQgbm9kZShzKQ0KICANCiAgbGlicmFyeShzZikNCiAgbGlicmFyeShsd2dlb20pDQogIGxpYnJhcnkoZHBseXIpDQogIGxpYnJhcnkocHVycnIpDQogIA0KICBuY3JzID0gc3RfY3JzKG5ldHdvcmspDQogIGJsYWRlID0gc3Rfc2Yobm9kZSkgJT4lDQogICAgbXV0YXRlKGdlb21ldHJ5ID0gc3RfZ2VvbWV0cnkoLikpICU+JSAjIGp1c3QgaW4gY2FzZSBnZW9tZXRyeSBjb2x1bW4gaXNuJ3QgbmFtZWQgZ2VvbWV0cnkNCiAgICBzdF9zZXRfZ2VvbWV0cnkoImdlb21ldHJ5IikgJT4lDQogICAgbXV0YXRlKA0KICAgICAgbmYgPSBzdF9uZWFyZXN0X2ZlYXR1cmUoLiwgbmV0d29yayksDQogICAgICAjIG5lYXJlc3QgbGluZXN0cmluZw0KICAgICAgbmwgPSBkby5jYWxsKGMsIG1hcDIoDQogICAgICAgIGdlb21ldHJ5LCBuZiwgIH4gc3RfbmVhcmVzdF9wb2ludHMoc3Rfc2ZjKC54LCBjcnMgPSBuY3JzKSwgbmV0d29ya1sueSwgXSkNCiAgICAgICkpLA0KICAgICAgIyBuZWFyZXN0IHBvaW50IHRvIGVkZ2UvbGluZXN0cmluZw0KICAgICAgbnAgPSBkby5jYWxsKGMsIG1hcDIoDQogICAgICAgIG5sLCBnZW9tZXRyeSwgfiBzdF9jYXN0KHN0X3NmYygueCwgY3JzID0gbmNycyksICJQT0lOVCIpICU+JSAuWy4gIT0gc3Rfc2ZjKC55LCBjcnMgPSBuY3JzKV0NCiAgICAgICkpLA0KICAgICAgIyBuZWFyZXN0IHBvaW50IGluIG5lYXJlc3QgZWRnZS9saW5lc3RyaW5nDQogICAgICBucyA9IGRvLmNhbGwoYywgbWFwMigNCiAgICAgICAgbmYsIG5wLCB+IHN0X2Nhc3Qoc3RfZ2VvbWV0cnkobmV0d29ya1sueCwgXSksICJQT0lOVCIpICU+JSAuW3N0X25lYXJlc3RfZmVhdHVyZShzdF9zZmMoLnksIGNycyA9IG5jcnMpLCAuKV0NCiAgICAgICkpLA0KICAgICAgbGVuID0gc3RfbGVuZ3RoKG5sKSAlPiUgdW5pdHM6OmRyb3BfdW5pdHMoKQ0KICAgICkgJT4lDQogICAgZmlsdGVyKG5wICE9IG5zKSAlPiUgIyByZW1vdmUgcm93cyB3aGVyZSBuZWFyZXN0IHBvaW50IGlzIGFscmVhZHkgaW4gbmVhcmVzdCBlZGdlL2xpbmVzdHJpbmcNCiAgICBzdF9zZXRfZ2VvbWV0cnkoIm5sIikgJT4lDQogICAgc2VsZWN0KG5mLCBsZW4pICU+JQ0KICAgIHN1cHByZXNzV2FybmluZ3MoKQ0KICANCiAgYmxhZGUgPSBzdF9nZW9tZXRyeShibGFkZSkgJT4lDQogICAgew0KICAgICAgKC4gLSBzdF9jZW50cm9pZCguKSkgKiAoYmxhZGUkbGVuICogMiAvIGJsYWRlJGxlbikgKyBzdF9jZW50cm9pZCguKSAjIGVuc3VyZSBsaW5lIGlzIGxvbmcgZW5vdWdoIHRvIHNwbGl0IGVkZ2UNCiAgICB9ICU+JQ0KICAgIHN0X3NldF9jcnMobmNycykgJT4lDQogICAgc3Rfc2YoKSAlPiUNCiAgICBtdXRhdGUobmYgPSBibGFkZSRuZikNCiAgDQogIHN0X3NwbGl0KG5ldHdvcmtbYmxhZGUkbmYsIF0sIGJsYWRlKSAlPiUNCiAgICBzdF9jb2xsZWN0aW9uX2V4dHJhY3QoIkxJTkVTVFJJTkciKSAlPiUNCiAgICByYmluZChuZXR3b3JrWy1ibGFkZSRuZiwgXSkNCn0NCmBgYA0KYHJvd3dpc2VgIGZyb20gYGRwbHlyYCBjb3VsZCBoYXZlIGJlZW4gdXNlZCBpbiBwbGFjZSBvZiB0aGUgYHB1cnJyYCBmdW5jdGlvbnMgaGVyZSwgdGhvdWdoIGBtYXAyYCBlbmRlZCB1cCBiZWluZyBmYXN0ZXIuIA0KYGBge3IgbXVsdGlwYXJ0cyB0byBzaW5nbHBhcnRzLCBlY2hvPUZ9DQpzdF9tdWx0aXBhcnRfdG9fc2luZ2xlcGFydHMgPSBmdW5jdGlvbih4KSB7DQogICMgUmVwbGljYXRlcyBNdWx0aXBhcnQgdG8gU2luZ2xlcGFydHMgYWxnb3JpdGhtIGluIFFHSVMNCiAgbGlicmFyeShzZikNCiAgDQogIG1peGVkID0gc3Rfc2YoeCkNCiAgbWl4ZWQkaWQgPSAxOm5yb3cobWl4ZWQpDQogIGdlb190eXBlcyA9IGFzLmNoYXJhY3Rlcih1bmlxdWUoc3RfZ2VvbWV0cnlfdHlwZShtaXhlZCkpKQ0KICBzdHJfZ2VvID0gZ3JlcGwoIk1VTFRJIiwgZ2VvX3R5cGVzKQ0KICANCiAgaWYgKHN1bShzdHJfZ2VvKSA9PSAwKSB7DQogICAgcmV0dXJuKHgpDQogIH0NCiAgDQogIG11bHRpID0gZ2VvX3R5cGVzW3N0cl9nZW9dDQogIA0KICBpZiAoc3VtKHN0cl9nZW8pID09IGxlbmd0aChnZW9fdHlwZXMpKSB7DQogICAgc2luZ2xlID0gZ3N1YigiTVVMVEkiLCAiIiwgbXVsdGkpDQogIH0gZWxzZSB7DQogICAgc2luZ2xlID0gZ2VvX3R5cGVzWyFzdHJfZ2VvXQ0KICB9DQogIA0KICBtdWx0aXBhcnQgPSBtaXhlZFtzdF9nZW9tZXRyeV90eXBlKG1peGVkKSA9PSBtdWx0aSxdDQogIGlkID0gbXVsdGlwYXJ0JGlkDQogIHJvd3MgPSAxOm5yb3cobXVsdGlwYXJ0KQ0KICANCiAgc2luZ2xlcGFydCA9IGRvLmNhbGwocmJpbmQsIGxhcHBseShyb3dzLCBmdW5jdGlvbihzKQ0KICAgIHN1cHByZXNzV2FybmluZ3Moc3RfY2FzdCgNCiAgICAgIG11bHRpcGFydFtzLF0sIHNpbmdsZQ0KICAgICkpKSkNCiAgDQogIGNvbCA9IGNvbG5hbWVzKG1peGVkKQ0KICByYmluZChtaXhlZFstaWQsIF0sIHNpbmdsZXBhcnQpWyxjb2xbY29sIT0iaWQiXV0NCn0NCmBgYA0KDQpgYGB7ciBnZXR0aW5nIHNvbWUgZGF0YSBhbmQgc3BsaXR0aW5nIHN0cmVldHMsIG1lc3NhZ2UgPSBGLCB3YXJuaW5nID1GLGZpZy5oZWlnaHQ9NiwgZmlnLndpZHRoPTl9DQpsaWJyYXJ5KHNmKQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkodGlncmlzKQ0KbGlicmFyeShtYXB2aWV3KQ0KDQphZGRpc29uID0gY291bnRpZXMoInZ0IiwgcHJvZ3Jlc3MgPSBGKSAlPiUgZmlsdGVyKENPVU5UWUZQID09ICIwMDEiKSAlPiUgc3RfdHJhbnNmb3JtKDMyMTQ1KQ0KDQpzY2hvb2xzID0gcmVhZF9zZigNCiAgImh0dHBzOi8vb3BlbmRhdGEuYXJjZ2lzLmNvbS9kYXRhc2V0cy9lZmE3OTgzOWIwZjg0MWFjOTJjODIxYTdiOGFmZWRhNV8zLmdlb2pzb24iDQopICU+JQ0KICBzdF90cmFuc2Zvcm0oMzIxNDUpICU+JQ0KICBmaWx0ZXIobGVuZ3RocyhzdF9pbnRlcnNlY3RzKC4sIGFkZGlzb24pKSAhPSAwKQ0KDQpzdHJlZXRzID0gcmVhZF9zZigNCiAgImh0dHBzOi8vb3BlbmRhdGEuYXJjZ2lzLmNvbS9kYXRhc2V0cy8xZGVlNWNiOTM1ODk0ZjlhYmUxYjhlN2NjZWMxMjUzZV8zOS5nZW9qc29uIg0KKSAlPiUNCiAgc3RfdHJhbnNmb3JtKDMyMTQ1KSAlPiUNCiAgc3RfaW50ZXJzZWN0aW9uKHN0X2dlb21ldHJ5KGFkZGlzb24pKSAlPiUNCiAgc3RfbXVsdGlwYXJ0X3RvX3NpbmdsZXBhcnRzKCkgJT4lICMgbmVlZCB0byBtYWtlIHN1cmUgYWxsIGZlYXR1cmVzIGFyZSBsaW5lc3RyaW5ncyAgDQogIG11dGF0ZShpZCA9IHJvd19udW1iZXIoKSkNCg0KYmxlbmRlZCA9IHN0X2JsZW5kKHNjaG9vbHMsIHN0cmVldHMpICU+JQ0KICBncm91cF9ieShpZCkgJT4lDQogIG11dGF0ZShjbnQgPSBuKCkpICU+JQ0KICB1bmdyb3VwDQoNCnNwbGl0X3N0cmVldHMgPSBmaWx0ZXIoYmxlbmRlZCwgY250ID4gMSkgJT4lDQogIG11dGF0ZShjb2xvciA9IGlmZWxzZShhcy5udW1lcmljKHJvdy5uYW1lcyguKSkgJSUgMiAhPSAwLCAic3BsaXQiLCAic3RyZWV0IikpDQoNCnVuc3BsaXQgPSBmaWx0ZXIoYmxlbmRlZCwgY250ID09IDEpDQoNCm1hcHZpZXcoDQogIHNlbGVjdChzcGxpdF9zdHJlZXRzLCBjb2xvciksDQogIGxheWVyLm5hbWUgPSAiU3BsaXQgU3RyZWV0cyIsDQogIGNvbG9yID0gYygiI0YzRDU0RSIsICIjNjlCM0U3IikNCikgKw0KICBtYXB2aWV3KHVuc3BsaXQsIGxheWVyLm5hbWUgPSAiVW5zcGxpdCBTdHJlZXRzIiwgY29sb3IgPSAiIzRDNEI0QyIpICsNCiAgbWFwdmlldygNCiAgICBzY2hvb2xzLA0KICAgIGxheWVyLm5hbWUgPSAiQWRkaXNvbiBDb3VudHkgU2Nob29scyIsDQogICAgY29sLnJlZ2lvbnMgPSAiIzBEMzk1RiIsDQogICAgY29sb3IgPSAiIzAwMDAwMCIsDQogICAgYWxwaGEgPSAuMg0KICApDQpgYGANCg0KSW4gdGhpcyBleGFtcGxlLCB0aGUgbGluZXN0cmluZ3MgbmVhcmVzdCB0byB0aGUgc2Nob29scyBhcmUgZWl0aGVyIHNwbGl0IG9yIGFyZSBsZWZ0IHVudG91Y2hlZCBpZiB0aGUgbmVhcmVzdCBwb2ludCBmcm9tIHRoZSBzY2hvb2wgYWxyZWFkeSBleGlzdHMgYXMgYSB2ZXJ0ZXggaW4gdGhlIGxpbmVzdHJpbmcuIEkgZm91bmQgdGhhdCBgc2ZuZXdvcmtzYCBoYXMgYSBmdW5jdGlvbiBjYWxsZWQgW2BzdF9uZXR3b3JrX2JsZW5kYF0oaHR0cHM6Ly9sdXVrdmRtZWVyLmdpdGh1Yi5pby9zZm5ldHdvcmtzL3JlZmVyZW5jZS9zdF9uZXR3b3JrX2JsZW5kLmh0bWwpIGFmdGVyIHdyaXRpbmcgdGhpcyBzbyBJIG1heSB1c2UgdGhhdCBpbiB0aGUgZnV0dXJlLCB0aG91Z2ggd3JpdGluZyBgc3RfYmxlbmRgIHdhcyBzdGlsbCBhIGZ1biBleGVyY2lzZS4=