DataTourisme flux field audit
Historical analysis
This is a point-in-time measurement, published on 2026-08-04 against a snapshot of the flux
dated 2026-07-18. The figures describe that snapshot, not the current flux. DataTourisme is
still imported by the provisioner (provisioner/src/DataTourismeImporter.php,
DataTourismeMapper.php), and as of 2026-09-27 the recommendations below have not been
implemented: the Accueil Vélo label and hasPricingMode are still not imported, and the
mapping defects of section 5 are still present.
Read-only measurement of the DataTourisme national flux we already download, run for issue #879. It answers three questions with numbers rather than assumptions:
- Does the flux carry the Accueil Vélo label?
- Does it carry a minimum stay or booking granularity, the reserve that keeps the
rentalaccommodation type opt-in? - Is the flux itself under-configured, dropping data upstream of our mapper?
Short answers: yes, no, no. The detail below also decides two sprint-50 source questions and
uncovers four mapping defects in DataTourismeMapper.
Corpus and method
| Property | Value |
|---|---|
| Flux | #26308 (flux-26308-*.zip, from the Content-Disposition of the webservice URL) |
| Snapshot audited | objects dated 2026-07-18, downloaded 2026-07-22 |
| Newer generation available | 2026-08-03 20:53 (same URL, so a refresh is a download away) |
| Archive | 1.85 GiB zipped, 10.8 GB uncompressed |
| Objects | 375,748 JSON-LD files under objects/, plus index.json (375,748 entries) and context.jsonld |
| Vocabulary | context.jsonld declares 218 terms — the exhaustive list of predicates this flux can express |
Every figure below comes from a full pass over the 375,748 objects (no sampling), with the object → table
classification replicated from DataTourismeMapper::classify() so the buckets match what we actually import.
Percentages are shares of objects in the bucket unless the row says "occurrences" (a predicate can repeat
inside one object).
1. Key inventory and fill rates
What the flux splits into
| Bucket (our mapping) | Objects | Share |
|---|---|---|
accommodations |
124,036 | 33.0% |
events |
87,861 | 23.4% |
food_pois |
61,534 | 16.4% |
cultural_pois |
55,550 | 14.8% |
dropped — Store without a food subtype |
39,026 | 10.4% |
dropped — Tour / Product / Practice and other non-place objects |
7,537 | 2.0% |
dropped — Accommodation with an unmapped subtype |
204 | 0.1% |
| imported | 328,981 | 87.6% |
| dropped | 46,767 | 12.4% |
Accommodations by app category: rental 82,392 · hotel 13,176 · guest_house 12,591 · camp_site 9,844 ·
hostel 5,827 · chalet 123 · wilderness_hut 83.
Accommodations — all 38 top-level predicates
| Predicate | Objects | Fill | Mapped today |
|---|---|---|---|
dc:identifier |
124,036 | 100.0% | no |
rdfs:label |
124,036 | 100.0% | yes (name) |
hasBeenCreatedBy |
124,036 | 100.0% | no |
hasBeenPublishedBy |
124,036 | 100.0% | no |
isLocatedAt |
124,036 | 100.0% | yes (geometry, address, hours) |
lastUpdate |
124,036 | 100.0% | no |
lastUpdateDatatourisme |
124,036 | 100.0% | no |
hasContact |
123,895 | 99.9% | yes (website, phone, email) |
hasDescription |
108,836 | 87.7% | yes (shortDescription) |
offers |
96,910 | 78.1% | partially (price only) |
hasFeature |
82,148 | 66.2% | no — see defect 2 |
creationDate |
76,797 | 61.9% | no |
allowedPersons |
75,588 | 60.9% | yes (capacity) |
hasReview |
72,556 | 58.5% | no — carries Accueil Vélo |
rdfs:comment |
70,835 | 57.1% | yes (description fallback) |
hasBookingContact |
66,404 | 53.5% | yes (booking_url) |
hasTranslatedProperty |
64,980 | 52.4% | no |
availableLanguage |
60,865 | 49.1% | no |
hasRepresentation |
54,582 | 44.0% | no |
hasMainRepresentation |
50,732 | 40.9% | yes (image_url) |
isOwnedBy |
50,164 | 40.4% | no |
hasTheme |
27,722 | 22.3% | yes (labels) |
hasNeighborhood |
22,726 | 18.3% | no |
hasExternalReference |
17,116 | 13.8% | no |
hasAudience |
3,602 | 2.9% | no |
hasClientTarget |
2,262 | 1.8% | no |
reducedMobilityAccess |
2,139 | 1.7% | no |
hasManagementContact |
1,407 | 1.1% | no |
hasCommunicationContact |
1,382 | 1.1% | no |
hasGeographicReach |
1,340 | 1.1% | no |
providesCuisineOfType |
870 | 0.7% | no |
takeAway |
173 | 0.1% | no |
COVID19SpecialMeasures |
48 | 0.0% | no |
hasAdministrativeContact |
47 | 0.0% | no |
owl:sameAs |
13 | 0.0% | yes (wikidata) |
hasFloorSize |
13 | 0.0% | no |
schema:legalName |
1 | 0.0% | no |
hasArchitecturalStyle |
1 | 0.0% | no |
Nested paths worth naming (occurrences, not objects): hasContact.foaf:homepage 106,140 ·
hasBookingContact.foaf:homepage 41,524 · hasFeature.features 456,811 ·
isLocatedAt.schema:openingHoursSpecification 55,845 · hasReview.hasReviewValue 107,526 ·
isLocatedAt.schema:address.schema:streetAddress 121,772.
The other buckets, top predicates
| Predicate | cultural (55,550) |
food (61,534) |
event (87,861) |
dropped Store (39,026) |
|---|---|---|---|---|
rdfs:label, isLocatedAt, hasBeenCreatedBy, lastUpdate |
100.0% | 100.0% | 100.0% | 100.0% |
hasContact |
92.5% | 100.0% | 100.0% | 99.5% |
hasDescription |
94.1% | 90.6% | 98.8% | 90.6% |
takesPlaceAt |
0.0% | 0.0% | 100.0% | 0.0% |
schema:startDate / schema:endDate |
0.0% | 0.0% | 100.0% | 0.0% |
offers |
28.3% | 57.2% | 62.1% | 18.4% |
hasTheme |
50.6% | 54.3% | 29.1% | 32.5% |
hasFeature |
20.0% | 36.1% | 13.1% | 7.1% |
hasReview |
11.2% | 7.2% | 0.5% | 3.7% |
hasBookingContact |
28.4% | 31.5% | 36.1% | 17.0% |
hasMainRepresentation |
33.3% | 23.4% | 47.6% | 18.0% |
owl:sameAs |
6.9% | 0.0% | 0.0% | 0.0% |
Two structural facts hold across every bucket: 100% of objects are geolocated and 100% carry a
rdfs:label. Neither is a filter we asked for — it is how the national base is published.
2. Accueil Vélo: present, and currently thrown away
The label is a review concept, not a classification:
"hasReview": [{
"@type": ["Review"],
"hasReviewValue": {
"@id": "kb:LabelRating_AccueilVelo",
"@type": ["LabelRating"],
"rdfs:label": {"fr": ["Accueil Vélo"], "en": ["Accueil Vélo"]}
}
}]
kb:LabelRating_AccueilVelo appears on 8,010 objects, every one of them at
hasReview[].hasReviewValue.@id:
| Bucket | Objects with the label | Share of bucket |
|---|---|---|
| accommodations | 6,006 | 4.8% |
dropped Store |
764 | 2.0% |
food_pois |
667 | 1.1% |
cultural_pois |
551 | 1.0% |
| other / unmapped | 22 | — |
Per accommodation category:
| Category | Labelled | Share |
|---|---|---|
hotel |
1,706 | 12.9% |
camp_site |
1,023 | 10.4% |
wilderness_hut |
8 | 9.6% |
hostel |
552 | 9.5% |
guest_house |
890 | 7.1% |
rental |
1,827 | 2.2% |
chalet |
0 | 0.0% |
Coverage is essentially complete. The published size of the national network is roughly 7,100–8,500 labelled providers — Ille-et-Vilaine Tourisme cites more than 7,100, the DGE more than 8,000 — around two thirds of them accommodations. The flux carries 8,010 labelled objects including 6,006 accommodations. There is no measurable gap left for a dedicated source to fill, so scraping or licensing francevelotourisme.com is not justified.
Only 14 objects mention "accueil vélo" in prose without the structured concept, so a text heuristic adds nothing.
The label is not the only bike signal, and the signals are largely disjoint — combining them recalls far more than either alone:
| Signal (accommodations) | Objects | Share |
|---|---|---|
| Accueil Vélo label | 6,006 | 4.8% |
Locked bike room or garage (kb:BikeRoom, kb:BikeGarage) |
5,601 | 4.5% |
| Any bike amenity (room, garage, parking, hire, loan, repair, e-bike charging) | 10,012 | 8.1% |
Cyclist audience (kb:Cyclists, kb:CycleTourist, kb:MountainBikers) |
2,172 | 1.8% |
Bike theme (kb:Bike, kb:BicycleTouring, …) |
1,738 | 1.4% |
| Any of the above | 15,718 | 12.7% |
Overlap: 771 objects have both the label and a locked bike room, 5,235 have the label without one, and 4,830 have a locked bike room without the label.
The label vocabulary is rich beyond cycling — 82 distinct kb:LabelRating_* concepts, all in the same place
and all discarded today. The largest: GitesDeFrance 17,066 · AccueilVelo 8,010 ·
VignoblesDecouvertes 3,285 · MonumentHistorique 2,639 · QualiteTourisme 2,441 · ClefVerte 1,355 ·
CleVacances 1,102 · the four TourismeHandicap* pictograms 888–1,081 each · AccueilMotards 227 ·
AccueilCompostelle 142. Star ratings (kb:ScaleRating_*) sit in the same hasReview block.
3. Minimum stay: absent from the flux, and from the ontology it exposes
This is settled at the vocabulary level, not by sampling: the flux's context.jsonld declares 218 terms and
none of them expresses a stay length or a booking granularity. The closest terms are duration /
durationDays (used by itineraries, never by accommodations), appliesOnPeriod (a validity date range for a
price), arrivedAt, weekOfMonth, requiredMinPersonCount and occupancy (68 occurrences, all null). A
key-name scan across all 124,036 accommodations, to depth 5, matching
minim|stay|night|nuit|semaine|week|rental|granular|period|…, returned only unrelated unit fields
(hasFloorSize.schema:unitText = "m²", ebucore:heightUnit = "px", schema:dayOfWeek).
The only exploitable proxy is offers.schema:priceSpecification.hasPricingMode, a controlled vocabulary
(162,389 occurrences). Rental pricing modes, in order: kb:PerWeek 18,151 · kb:Overnight 6,269 ·
kb:Weekend 6,184 · kb:MidWeek 1,659 · kb:For3Weeks 1,082 · kb:Weekend2Nights 710 ·
kb:SubscriptionPackage 603 · kb:PerWeek7Nights 373 · kb:PerWeek6Nights 325 · kb:Fortnightly 275 ·
kb:For2Days 221 · kb:PerMonth 107 · kb:PerDay 8.
Classifying each accommodation by the modes it publishes — night-granular (Overnight, PerDay, Weekend*,
For2Days, HalfDayPackage), week-or-longer only (PerWeek*, Fortnightly, PerMonth, MidWeek,
SubscriptionPackage), a price with no time unit at all, or no price:
| Category | Night | Night + week | Week only | Price, no unit | No price |
|---|---|---|---|---|---|
rental (82,392) |
3,221 (3.9%) | 8,277 (10.0%) | 10,651 (12.9%) | 12,941 (15.7%) | 47,302 (57.4%) |
hotel (13,176) |
1,235 (9.4%) | 0 | 1 (0.0%) | 5,396 (41.0%) | 6,544 (49.7%) |
guest_house (12,591) |
2,290 (18.2%) | 98 (0.8%) | 36 (0.3%) | 3,066 (24.4%) | 7,101 (56.4%) |
camp_site (9,844) |
438 (4.4%) | 599 (6.1%) | 332 (3.4%) | 3,277 (33.3%) | 5,198 (52.8%) |
hostel (5,827) |
706 (12.1%) | 154 (2.6%) | 317 (5.4%) | 1,673 (28.7%) | 2,977 (51.1%) |
wilderness_hut (83) |
27 (32.5%) | 21 (25.3%) | 2 (2.4%) | 3 (3.6%) | 30 (36.1%) |
Verdict for the rental reserve (#865):
the flux does not let us flip the default. It supports three narrower moves:
- Exclude 10,651 rentals (12.9%) that price by the week or longer and never by the night — the Saturday-to-Saturday market the reserve was about.
- Promote 11,498 rentals (14.0%) that publish a night, day or weekend rate: those are demonstrably bookable for one stop.
- Leave the remaining 73.1% undetermined (15.7% price without a unit, 57.4% publish no price at all). No measurement in this flux can classify them.
Among the 6,006 Accueil Vélo accommodations the same blind spot holds: 2,441 publish no price and 2,409 a price with no unit, so the label cannot stand in for night-bookability either. Only 382 rentals are both night-priced and labelled.
4. Flux configuration: not the bottleneck
Measured on the archive itself:
- All four root branches are present —
PlaceOfInterest284,496,EntertainmentAndEvent87,861,Product27,595,Tour7,719,Practice3,315. A category-restricted flux would be missing whole branches; none is. - National geographic coverage — 17 distinct regions and 100 distinct departments appear in the postal addresses. No geographic filter is in play.
- The complete data model is served, not a simplified one:
hasReview,hasFeature.features,offers.schema:priceSpecification.hasPricingMode,hasTranslatedProperty, seven-language label maps and the full media blocks are all there. Nothing is truncated field-side. - Terms the ontology defines but the producers never populate:
nationalAddressId(0 occurrences — no BAN address id anywhere in the flux),hasCompletenessScore(0),isEquippedWith(0). - Terms populated but ignored by us:
textOpeningHoursSpecification(free-text hours: 7,240 accommodations, 5,294 food, 2,467 events, 1,530 cultural) andhasExternalIdentifier(17,075 accommodations, 17,698 food).
Recommendation: leave the flux configuration alone. There is nothing to widen upstream for the data we
consume; every loss measured here happens in our own mapper. Two operational notes: the local snapshot is
three weeks stale (a 2026-08-03 generation is already published), and data.gouv.fr offers the same base as a
daily bulk export (RDF N-Triples plus simplified CSV per branch: PLACE, FMA, PRODUCT, TOUR) with no
account, which is a viable fallback if the flux ever breaks.
What our own mapper drops, and what widening it would buy
| Dropped today | Objects | Worth importing? |
|---|---|---|
Market + CoveredMarket (as Store without a food subtype) |
8,850 | Yes — a weekly market is a resupply option, but it needs the day-of-week from openingHours to be useful. |
CyclingTour (inside the 7,537 non-place objects) |
7,525 | Only as metadata. Measured on 400 sampled tours: tourDistance 60%, positiveCumulDifference 25%, duration 22%, olo:slot 16% — and no track geometry, just a single start point. They are not routes. |
BoutiqueOrLocalShop, CraftsmanShop, EquipmentRentalShop, … |
~30,000 | No — no resupply or lodging value. |
Accommodation with an unmapped subtype |
204 | Marginal; already counted and reported by the provisioner. |
Pharmacy (237) and Fountain (475) exist in the flux but are far better covered by OSM; they are not a
reason to widen anything.
5. Mapping defects found while measuring
All four live in DataTourismeMapper and are measurable, not
stylistic:
hasClassificationdoes not exist in this flux.labels()reads it first; the term is absent from the 218-termcontext.jsonldand has 0 occurrences in 375,748 objects. Dead branch.hasFeaturelabels are one level deeper than the code looks. The predicate points at aFeatureSpecificationwrapper that carries nordfs:label; the labels live on the nestedfeatures[]concepts (253 distinct, e.g.kb:BikeRoom,kb:Wifi). So thelabelstag currently equalshasThemealone — 27,722 accommodations (22.3%) — and never contains a quality label, contrary to the docblock claim that "quality labels such as Accueil Vélo are published as classifications". Reaching the nested concepts plushasReview.hasReviewValuewould raiselabelscoverage from 22.3% to 82.6% on accommodations (and 60.4% cultural, 66.6% food).- Top-level
foaf:homepageis never populated (0 occurrences in every bucket). The website always comes fromhasContact/hasBookingContact, which the code does reach — the first lookup is simply dead. owl:sameAsis effectively absent on accommodations: 13 objects out of 124,036 (0.01%), versus 3,834 cultural POIs (6.9%). Theaccommodations.wikidatacolumn added in #872 will stay empty for DataTourisme rows, so any pairing logic that relies on a shared Q-ID between an OSM and a DataTourisme lodging cannot work in practice.
6. Recommendations, in order
- Import the label block. Read
hasReview[].hasReviewValueand store thekb:concept ids (not the translated labels) alongside the existinglabelstag. That is one predicate for 8,010 Accueil Vélo objects plus 82 other labels and the star ratings. Fix defects 1–2 in the same change. - Make Accueil Vélo a first-order ranking signal in
App\Accommodation\CandidateRanker, combined with the bike amenities (kb:BikeRoom,kb:BikeGarage, …): together they mark 15,718 accommodations (12.7%), and the two signals overlap on only 771. - Do not add a bike-label source. Coverage in the flux matches the national network size; a dedicated source would add licensing and freshness risk for no measurable recall.
- Keep
rentalopt-in, but usehasPricingModeto exclude the 10,651 week-only rentals and to rank up the 11,498 night-priced ones. Say explicitly in the UI that the rest is unknown. - Drop the BAN-linkage assumption for DataTourisme:
nationalAddressIdis empty, so a BAN join has to be built from the postal address, not from an identifier the flux is supposed to carry. - Refresh the snapshot and consider importing markets (8,850) as resupply POIs.
Appendix — reproducing the measurement
The audit is a streaming pass over the archive; nothing is written and no database is touched.
# Which flux, which generation, without downloading 1.85 GiB
curl -s -o /dev/null -D - -r 0-0 \
"https://diffuseur.datatourisme.fr/webservice/${DATATOURISME_FLUX_ID}/${DATATOURISME_APP_KEY}" | grep -i content-disposition
# The vocabulary the flux can express (218 terms) — where the minimum-stay question is settled
python3 -c "import zipfile,json; print(sorted(json.loads(zipfile.ZipFile('.docker/osm/data/datatourisme/datatourisme-flux.zip').read('context.jsonld'))))"
# Accueil Vélo occurrences, straight off the archive
python3 - <<'PY'
import zipfile, json
z = zipfile.ZipFile('.docker/osm/data/datatourisme/datatourisme-flux.zip')
hits = sum(b'kb:LabelRating_AccueilVelo' in z.read(n)
for n in z.namelist() if n.startswith('objects/') and n.endswith('.json'))
print(hits)
PY
The per-category and pricing-granularity tables come from the same loop with
DataTourismeMapper::classify() reimplemented in Python and the kb: concept ids collected per predicate
path; on 12 cores a full pass over the 375,748 objects takes about ten minutes.