Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions gtfs/spec/en/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,20 @@ For a sub-journey of two consecutive legs with a transfer, if the transfer match
| `to_network_id` | Foreign ID referencing `routes.network_id` or `networks.network_id`| **Required** | Matches a post-transfer leg that uses the specified route network. If specified, the same `from_network_id` must also be specified. |
| `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 that constitute the effective leg. <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 first leg in the effective leg and the arrival fare validation of the last leg in the effective leg.<br>`1` - Between the departure fare validation of the first leg in the effective leg and the departure fare validation of the last leg in the effective leg.<br>`2` - Between the arrival fare validation of the first leg in the effective leg and the departure fare validation of the last leg in the effective leg.<br>`3` - Between the arrival fare validation of the first leg in the effective leg and the arrival fare validation of the last leg in the effective leg.<br><br>When an effective leg with the same `from_network_id` and `to_network_id` is matched multiple times consecutively within a multi-leg journey, the `duration_limit` specified by the effective leg should be measured starting from the first matched 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. |

@felixguendling felixguendling Jun 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The router doesn't know when exactly the ticket will be validated (i.e. walking time between fare gate and departure/arrival of the vehicle vice versa).

So I would change departure fare validation to departure time (same for arrival).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for sharing your feedback @felixguendling. In fact, the wording is copied verbatim from the definition of duration_limit_type in fare_transfer_rules.txt. Therefore, both need to be changed if need be.

However, this was talked about in a few 2023 Fare Working Group Meetings when timeframes were being discussed.

The consensus seemed to be that in a real-world situation, the timeframe (or fare product transfer duration since it's similar) starts when tapping on the bus (0 min of delay) or at a fare gate (a few minutes of delay based on the size of the station, how long the rider will linger or if they are taking the first train). So the time limit for a fare product starts counting down when the rider taps. Therefore, the wording "fare validation" accurately describes the mechanism in real life.

In terms of router behaviour, there are ways of approximating the duration of the journey's walking-legs to be able to compare directly with duration_limit, among others:

  • Using pathways if they exist.
  • Approximating in-station walking time using OSM.
  • Providing default in-station walking times.

This is left to trip planners to choose how they approximate it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is left to trip planners to choose how they approximate it.

A good standard doesn't leave room for interpretation.

So I would propose to:

  • either state that the timetable times (arrival / departure) can be used as they are. E.g. because the ticketing system itself leaves some room for walking by having more permissive limits or an average walking time is already subtracted from the duration limit
  • or it's clearly stated that in-station walking times have to be calculated using GTFS pathways from fare gates (probably pathway_mode=6?) to tracks (or vice versa for arrival) - which would require a Fares v2 feed to always come with proper GTFS pathways.

I think that adding a duration_limit without a clearly defined way to measure durations (e.g. based on GTFS pathways; OSM is out-of-scope here I guess) isn't helpful if you want to consume the data.



#### Using Fare Leg Join Rules or Fare Transfer Rules
For certain cases, `fare_leg_join_rules.txt` can represent transfers more efficiently than `fare_transfer_rules.txt`.

You can use Fare Leg Join Rules for these cases:
- When different journey legs are treated as one fare leg in the real world. For example, when no transfer validation is needed in the transfer station, or for silent transfers in buses that keep the same fare.
- Where it simplifies transfer rules. E.g. In an origin-destination fare structure with a huge number of zone combinations, it is easier to join the journey's fare legs into one effective fare leg and look at the first and last stops of the whole journey, instead of pricing fare leg-by-fare leg and defining a large number of transfer rules.

You can use Fare Transfer Rules for:
- Simple fare structures where the main factors to define the fare are the duration of the journey and the number of journey legs taken.
- Fare structures containing fare products that do not support transfers.

### fare_transfer_rules.txt

Expand Down Expand Up @@ -569,8 +583,8 @@ To process the cost of a multi-leg journey:

| Field Name | Type | Presence | Description |
| ------ | ------ | ------ | ------ |
| `from_leg_group_id` | Foreign ID referencing `fare_leg_rules.leg_group_id` | Optional | Identifies a group of pre-transfer fare leg rules.<br><br>If there are no matching `fare_transfer_rules.from_leg_group_id` values to the `leg_group_id` being filtered, empty `fare_transfer_rules.from_leg_group_id` will be matched by default. <br><br>An empty entry in `fare_transfer_rules.from_leg_group_id` corresponds to all leg groups defined under `fare_leg_rules.leg_group_id` excluding the ones listed under `fare_transfer_rules.from_leg_group_id`|
| `to_leg_group_id` | Foreign ID referencing `fare_leg_rules.leg_group_id` | Optional | Identifies a group of post-transfer fare leg rules.<br><br>If there are no matching `fare_transfer_rules.to_leg_group_id` values to the `leg_group_id` being filtered, empty `fare_transfer_rules.to_leg_group_id` will be matched by default.<br><br>An empty entry in `fare_transfer_rules.to_leg_group_id` corresponds to all leg groups defined under `fare_leg_rules.leg_group_id` excluding the ones listed under `fare_transfer_rules.to_leg_group_id` |
| `from_leg_group_id` | Foreign ID referencing `fare_leg_rules.leg_group_id` | Optional | Identifies a group of pre-transfer fare leg rules.<br><br>If there are no matching `fare_transfer_rules.from_leg_group_id` values to the `leg_group_id` being filtered, empty `fare_transfer_rules.from_leg_group_id` will be matched by default. <br><br>An empty entry in `fare_transfer_rules.from_leg_group_id` corresponds to all leg groups defined under `fare_leg_rules.leg_group_id` excluding the ones listed under `fare_transfer_rules.from_leg_group_id`<br><br> To avoid overlap between fare transfer rules and fare leg join rules, if there exists a record in `fare_leg_join_rules.txt` which joins the `fare_leg_rules.network_id` of `fare_transfer_rules.from_leg_group_id` to the `fare_leg_rules.network_id` of `fare_transfer_rules.to_leg_group_id`, then it is forbidden to set a fare transfer rule from `fare_transfer_rules.from_leg_group_id` to `fare_transfer_rules.to_leg_group_id`.|
| `to_leg_group_id` | Foreign ID referencing `fare_leg_rules.leg_group_id` | Optional | Identifies a group of post-transfer fare leg rules.<br><br>If there are no matching `fare_transfer_rules.to_leg_group_id` values to the `leg_group_id` being filtered, empty `fare_transfer_rules.to_leg_group_id` will be matched by default.<br><br>An empty entry in `fare_transfer_rules.to_leg_group_id` corresponds to all leg groups defined under `fare_leg_rules.leg_group_id` excluding the ones listed under `fare_transfer_rules.to_leg_group_id`.<br><br> To avoid overlap between fare transfer rules and fare leg join rules, if there exists a record in `fare_leg_join_rules.txt` which joins the `fare_leg_rules.network_id` of `fare_transfer_rules.from_leg_group_id` to the `fare_leg_rules.network_id` of `fare_transfer_rules.to_leg_group_id`, then it is forbidden to set a fare transfer rule from `fare_transfer_rules.from_leg_group_id` to `fare_transfer_rules.to_leg_group_id`.|
| `transfer_count` | Non-zero integer | **Conditionally Forbidden** | Defines how many consecutive transfers the transfer rule may be applied to.<br><br>Valid options are:<br>`-1` - No limit.<br>`1` or more - Defines how many transfers the transfer rule may span.<br><br>If a sub-journey matches multiple records with different `transfer_count`s, then the rule with the minimum `transfer_count` that is greater than or equal to the current transfer count of the sub-journey is to be selected.<br><br>Conditionally Forbidden:<br>- **Forbidden** if `fare_transfer_rules.from_leg_group_id` does not equal `fare_transfer_rules.to_leg_group_id`.<br>- **Required** if `fare_transfer_rules.from_leg_group_id` equals `fare_transfer_rules.to_leg_group_id`. |
| `duration_limit` | Positive integer | Optional | Defines the duration limit of the transfer.<br><br>Must be expressed in integer increments of seconds.<br><br>If there is no duration limit, `fare_transfer_rules.duration_limit` must be empty. |
| `duration_limit_type` | Enum | **Conditionally Required** | Defines the relative start and end of `fare_transfer_rules.duration_limit`.<br><br>Valid options are:<br>`0` - Between the departure fare validation of the first leg in transfer sub-journey and the arrival fare validation of the last leg in transfer sub-journey.<br>`1` - Between the departure fare validation of the first leg in transfer sub-journey and the departure fare validation of the last leg in transfer sub-journey.<br>`2` - Between the arrival fare validation of the first leg in transfer sub-journey and the departure fare validation of the last leg in transfer sub-journey.<br>`3` - Between the arrival fare validation of the first leg in transfer sub-journey and the arrival fare validation of the last leg in transfer sub-journey.<br><br>When a transfer rule with the same `from_leg_group_id` and `to_leg_group_id` is matched multiple times consecutively within a multi-leg journey, the `duration_limit` specified by the rule should be measured starting from the first matched leg.<br><br>Conditionally Required:<br>- **Required** if `fare_transfer_rules.duration_limit` is defined.<br>- **Forbidden** if `fare_transfer_rules.duration_limit` is empty. |
Expand Down
Loading