Skip to content

Conversation

@skalexch
Copy link
Collaborator

@skalexch skalexch commented Apr 22, 2025

This PR adds distance-based fares, it covers two use cases:

  • Leg-by-leg distance fare.
  • Total journey distance fare (eg: TfNSW)

The PR has a few adjustments to the spec:

  1. Add fare_distance_units_traveled in stop_times.txt
  2. Add fields for matching min_distance_units abd max_distance_units in fare_leg_rules.txt
  3. Add min_distance_units and max_distance_units to the primary key of fare_leg_rules.txt
  4. Adjust fare_leg_join_rules.txt to cover distance-based legs and include duration_limit and duration_limit_type (similar to fare_transfer_rules.txt) to limit the fare leg within a time window.
  5. Restrict joining legs so that distance-based legs cannot be joined with non-distance-based legs.

For more details and examples, please refer to the distance-based fares proposal

The proposal doc includes an example showcasing how distance-based fares for TfNSW can be implemented in Fares v2.

Note: A previous version of this PR separated the leg join mechanism for distance-based legs in a file called fare_leg_distance_rules.txt to cover distance-based fares of the Netherlands (Miro). However, the mechanism was still insufficient to fully model the fare structure. Therefore, we simplified the PR to be able to model most fares around the world using fare_leg_rules.txt and fare_leg_join_rules.txt.

@skalexch skalexch added GTFS Schedule Issues and Pull Requests that focus on GTFS Schedule Change type: Functional Refers to modifications that significantly affect specification functionalities. Discussion Period The community engages in conversations to help refine and develop the proposal. labels Apr 22, 2025
@miklcct
Copy link
Contributor

miklcct commented Apr 23, 2025

I have a question about distance_leg_join_type=1 in the proposal. The proposal lists the Netherlands system as an example but it shows that a journey consisting of 3 legs on different operators will charge a total price of more than 3 legs on their own, and some of the mileages are double-counted after the transfers. (for example, if I travel 100 km by NS, than 1 km by GVB, the proposal document says that I will pay for 100 km on NS and 101 km on GVB) I don't think it can be right here, can anyone give me some authoritative source to support the finding?

From what I read before, the Netherland system is that, for transfers across operators, the distance-based fares are completely separate (no joining); for transfers within the same operator, the distance accumulates (i.e. distance_leg_join_type=0.

The whole distance_leg_join_type=1 graph modelling shows that the same mileage is double or even triple-charged as the number of transfers accumulate.

@skalexch
Copy link
Collaborator Author

skalexch commented May 1, 2025

@miklcct thank you for your remark. After reviewing the modeling for the Netherlands' fares, you are absolutely correct. It turns out that the current proposal misses a key piece in the fare calculation.

As referenced in the notes for the 26/11/2024 Fares WGM, @skinkie shared an example with an equation explaining how the fare works.

E.g. in the case of a GVB (4 km) + Arriva (2 km) trip, the fare should be calculated as follows:
GVB_fare(4km) + Arriva_fare(6km) - Arriva_fare(4km).

The current proposal overlooked - Arriva_fare(4km).

We'll write something up to address this and try to figure out where this complex case fits in the proposal, or if other alternatives can be considered to address it. We'll share it soon. @skinkie can also probably provide more context into how NL fares are calculated.

@skalexch
Copy link
Collaborator Author

Hello @miklcct . As promised, we have created a document exploring the possibilities of modelling the Netherlands’ case and the limitations of Fares v2 in this context.

The main outcomes are as follows:

  • It might be possible to model the fare structure, but this approach would lead to the same bloated fare_transfer_rules.txt file that we tried to avoid by using fare_leg_distance_rules.txt.
  • We suggest focusing only on modelling the first case (total journey pricing) and implementing it in fare_leg_join_rules.txt. This will simplify the solution.
  • As for the Netherlands, it would be valuable to consider a specific extension that models its fare structure.

The full investigation is available here. This is also relevant for @skinkie to take a look at.

We can discuss this at the next working group meeting as well.

@eliasmbd eliasmbd added the Former Governance Applies This proposal is subject to the former governance process which predates July 7, 2025. label Jul 7, 2025
@skalexch
Copy link
Collaborator Author

skalexch commented Nov 7, 2025

Based on the conversation from October's Fares working group, the following adjustments were made:

  • Removed distance_type and used the term distance units (min_distance_units, max_distance_units,fare_distance_units_traveled).
  • Added a definition of "distance units" to explain that it can be both physical distance (km, m,m miles), other incremental (stop count) or any arbitrary measure that includes fare increments or discounts.
  • Changed the type of all distance-related fields to non-negative integer.
  • Removed the need for fare_distance_units_traveled to increase with stop_sequence --> This was not discussed in the meeting, but it makes sense to remove this constraint if we want to allow distance unit discounts.

| `continuous_pickup` | Enum | **Conditionally Forbidden** | Indicates that the rider can board the transit vehicle at any point along the vehicle’s travel path as described by [shapes.txt](#shapestxt), from this `stop_time` to the next `stop_time` in the trip’s `stop_sequence`. Valid options are: <br><br>`0` - Continuous stopping pickup. <br>`1` or empty - No continuous stopping pickup. <br>`2` - Must phone agency to arrange continuous stopping pickup. <br>`3` - Must coordinate with driver to arrange continuous stopping pickup. <br><br>If this field is populated, it overrides any continuous pickup behavior defined in [routes.txt](#routestxt). If this field is empty, the `stop_time` inherits any continuous pickup behavior defined in [routes.txt](#routestxt).<br><br>**Conditionally Forbidden**:<br>- **Forbidden** if `start_pickup_drop_off_window` or `end_pickup_drop_off_window` are defined.<br> - Optional otherwise. |
| `continuous_drop_off` | Enum | **Conditionally Forbidden** | Indicates that the rider can alight from the transit vehicle at any point along the vehicle’s travel path as described by [shapes.txt](#shapestxt), from this `stop_time` to the next `stop_time` in the trip’s `stop_sequence`. Valid options are: <br><br>`0` - Continuous stopping drop off. <br>`1` or empty - No continuous stopping drop off. <br>`2` - Must phone agency to arrange continuous stopping drop off. <br>`3` - Must coordinate with driver to arrange continuous stopping drop off. <br><br>If this field is populated, it overrides any continuous drop-off behavior defined in [routes.txt](#routestxt). If this field is empty, the `stop_time` inherits any continuous drop-off behavior defined in [routes.txt](#routestxt).<br><br>**Conditionally Forbidden**:<br>- **Forbidden** if `start_pickup_drop_off_window` or `end_pickup_drop_off_window` are defined.<br> - Optional otherwise. |
| `shape_dist_traveled` | Non-negative float | Optional | Actual distance traveled along the associated shape, from the first stop to the stop specified in this record. This field specifies how much of the shape to draw between any two stops during a trip. Must be in the same units used in [shapes.txt](#shapestxt). Values used for `shape_dist_traveled` must increase along with `stop_sequence`; they must not be used to show reverse travel along a route.<br><br>Recommended for routes that have looping or inlining (the vehicle crosses or travels over the same portion of alignment in one trip). See [`shapes.shape_dist_traveled`](#shapestxt). <hr>*Example: If a bus travels a distance of 5.25 kilometers from the start of the shape to the stop,`shape_dist_traveled`=`5.25`.*|
| `fare_distance_units_traveled` | Non-negative integer | Conditionally Required | Fare distance units traveled along the trip, from the first stop to the stop specified in this record. `fare_distance_units_traveled` is used to match a fare leg rule from [fare_leg_rules.txt](#fare_leg_rulestxt) whose distance interval [`min_distance_units`, `max_distance_units`) includes `fare_distance_units_traveled`. <br><br> `fare_distance_units_traveled` does not have to be in the same unit as `stop_times.shape_dist_traveled`. <br><br> If the fare is calculated based on the distance covered by the route shape, `fare_distance_units_traveled` may correspond to `stop_times.shape_dist_traveled` (measured in meters) or to its rounded value (floor or ceiling), depending on the fare structure. If the fare is calculated based on stops, `fare_distance_units_traveled` may represent the number of stops crossed. `fare_distance_units_traveled` may also be adjusted by adding or subtracting arbitrary values to represent fare discounts or supplements. <br><br> **Conditionally Required** <br>- Required if the `route_id` of the `trip_id` of this stop time is part of a `network_id` which is associated to a distance-based `leg_group_id` in `fare_leg_rules.txt`.<br> - Forbidden otherwise.|
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fare_distance_units_traveled is used to match a fare leg rule from fare_leg_rules.txt whose distance interval [min_distance_units, max_distance_units) includes fare_distance_units_traveled.

This may be interpreted as matching the cumulative distance from the first stop? But passengers may board mid-route.

nit: [min_distance_units, max_distance_units) bracket consistency

| `from_stop_id` | Foreign ID referencing `stops.stop_id`| **Conditionally Required** | Matches a pre-transfer leg that ends at the specified stop (`location_type=0` or empty) or station (`location_type=1`).<br><br>Conditionally Required:<br> - **Required** if `to_stop_id` is defined.<br> - Optional otherwise. |
| `to_stop_id` | Foreign ID referencing `stops.stop_id`| **Conditionally Required** | Matches a post-transfer leg that starts at the specified stop (`location_type=0` or empty) or station (`location_type=1`).<br><br>Conditionally Required:<br> - **Required** if `from_stop_id` is defined.<br> - Optional otherwise. |
| `duration_limit` | Positive integer | **Optional** | Defines the duration limit of the transfer between the legs.<br><br>Must be expressed in integer increments of seconds.<br><br>If there is no duration limit, `fare_leg_join_rules.duration_limit` must be empty. |
| `duration_limit_type` | Enum | **Conditionally Required** | Defines the relative start and end of `fare_leg_join_rules.duration_limit`.<br><br>Valid options are:<br>`0` - Between the departure fare validation of the current leg and the arrival fare validation of the next leg.<br>`1` - Between the departure fare validation of the current leg and the departure fare validation of the next leg.<br>`2` - Between the arrival fare validation of the current leg and the departure fare validation of the next leg.<br>`3` - Between the arrival fare validation of the current leg and the arrival fare validation of the next leg.<br><br>Conditionally Required:<br>- Required if `fare_leg_join_rules.duration_limit` is defined.<br>- Forbidden if `fare_leg_join_rules.duration_limit` is empty. |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps replace "current" and "next" by "first" and "last"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Change type: Functional Refers to modifications that significantly affect specification functionalities. Discussion Period The community engages in conversations to help refine and develop the proposal. Former Governance Applies This proposal is subject to the former governance process which predates July 7, 2025. GTFS Schedule Issues and Pull Requests that focus on GTFS Schedule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants