A public dataset, read by one tool
Speed-camera warnings are the one piece of road knowledge Pathfinder brings along itself, and it takes them from a public source: the Korean National Police Agency's standard dataset of fixed unmanned traffic-enforcement cameras (전국무인교통단속카메라표준데이터, dataset 15028200 on data.go.kr). The portal describes it as fixed cameras only, mobile units excluded, with the police supplying speed, signal, passage and other cameras and local governments supplying bus-lane and parking ones, refreshed about twice a year.
The app never calls that API. One executable in PathfinderKit, pathfinder-cameras, is the only writer of the camera file the app bundles. fetch pages through the API a thousand rows at a time with a key read from the environment or from cwkPippa's env file, writes the compact bundle, and prints a table; verify prints the same table from a saved raw pull. No key ships in the app. A refresh is one fetch and one new build.
Why only one writer
The dataset says what each camera enforces with a code, regltSe, and Pathfinder's meaning for those codes is inferred, not quoted. The full pull answered the question with its own numbers: codes 1 and 2 carry real limits (speed, and signal cameras that mostly enforce speed as well), code 4 is mostly limit 0 (parking), code 3 is limit 0 and stands at bus stops (bus lane), and code 99 carries expressway section cameras at 100-110. Bus lanes and parking are dropped as not driving-relevant.
An inference can go stale without making a sound. If the police renumber a code, a hand-edited JSON or a second import script would carry the old meaning forward silently. So the tool that writes the file is also the tool that re-checks the inference: every fetch prints the code-by-limit table, and a change of meaning shows up as a row that no longer looks like its label. One writer means one place where that check cannot be skipped.
The same writer cleans what the source mixes. Section lengths arrive in kilometres on most rows and in metres on some, so a value under 100 is read as kilometres (no Korean average-speed section is 100 km long, and none is under 100 m). Rows outside Korea's latitude and longitude box are dropped. Management numbers repeat between institutions and even within one, so a camera's id is the institution code plus the number, with a suffix when it repeats.
The pull that ships had 43,724 rows. 32,184 of them are driving-relevant and bundled, and 1,100 of those are section-enforcement cameras: 551 where a section starts and 549 where one ends.
The provenance the doc promises and the data does not carry
Invariant 8 says the bundled set names its source, its dataset modified date, when it was fetched and the range of per-row reference dates. The model agrees: CameraDataset has a datasetModified field. But the code that builds the bundle passes datasetModified: nil every time, and the encoder leaves a nil optional out, so the shipped header carries the source, the fetch time and the newest and oldest reference dates, and no dataset modified date at all. The portal page shows one; the tool never asks for it.
Teach the code, and keep the lesson it carries: a provenance field that its only writer always fills with nil is a promise in a document, not a fact in the data. The fix, when it comes, belongs in the writer, which is exactly why there is only one.
Which cameras are on this road
CameraIndex files the cameras in a grid of 0.01° cells, about 1.1 km north to south and 0.9 km east to west in Korea, so "what is near this road" never scans the country. It samples the route every 400 m, collects the cameras in each sample's cell and the eight around it, and projects each candidate only onto the stretch that runs from 1,000 m before the first sample that saw it to 1,000 m past the last. Projecting every candidate onto the whole line had cost tens of millions of segment tests on the main actor for a long route, every time a route was chosen or previewed.
A camera counts when it lands within 30 m of the route on the driving side. The dataset's direction code is not geometry, so a rule stands in for it: traffic in Korea keeps right, so a pole for this direction stands to the right of the line or overhead, and a camera more than 10 m to the left belongs to the opposite carriageway. The side is a signed cross-track distance, positive to the right of travel.
What she says, and where
CameraWatch warns in two modes. On a guided drive it places the cameras on the route once and warns by distance along it. In the free camera drive, with no route at all, it watches a 25° cone ahead of the car's course, at most 35 m off the course line, above 5 m/s. The first warning comes at 500 m, or at 800 m when the car is doing 19.4 m/s or more (about 70 km/h): "500미터 앞에 과속 단속 카메라 제한속도 60". Within 200 m, if the car is more than 3 km/h over the limit, an urgent "아빠, 속도 줄여" follows. Each camera is warned at most once per stage. Section cameras say where enforcement begins and ends: "구간 단속 시작" and "구간 단속 끝".
A walk hears no camera warnings, and a trip Google guides has none, because Google shows its own limits on its own map. Since #401 the free camera drive is Korea's alone. The button appears only when the phone's own fix is in South Korea, never by the map's centre or a simulated position; the resolved region is consulted only before the first fix, and a drive already running always stays stoppable. In Seville a button that only ever answered "Korea only", and looked like the talk's camera, was noise. Its glyph is now a speedometer.