From f8c03d57c2a2db4bd8bac35c9ec83bc1bc0bcab5 Mon Sep 17 00:00:00 2001 From: Skander Chouchene Date: Thu, 7 May 2026 10:42:18 -0400 Subject: [PATCH 1/6] [GTFS-Fares v2] Add duration limit to Fare Leg Join Rules and clarify usage --- gtfs/spec/en/reference.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/gtfs/spec/en/reference.md b/gtfs/spec/en/reference.md index 164a9a80..0ce8704b 100644 --- a/gtfs/spec/en/reference.md +++ b/gtfs/spec/en/reference.md @@ -541,6 +541,19 @@ 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`).

Conditionally Required:
- **Required** if `to_stop_id` is defined.
- 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`).

Conditionally Required:
- **Required** if `from_stop_id` is defined.
- Optional otherwise. | +| `duration_limit` | Positive integer | **Optional** | Defines the duration limit of the transfer between the legs that constitute the effective leg.

Must be expressed in integer increments of seconds.

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`.

Valid options are:
`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.
`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.
`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.
`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.

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.

Conditionally Required:
- **Required** if `fare_leg_join_rules.duration_limit` is defined.
- **Forbidden** if `fare_leg_join_rules.duration_limit` is empty. | + + +#### 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 legs are treated as one 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 legs and look at the first and last stops of the whole journey, instead of pricing leg-by-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_transfer_rules.txt From 5596cab73ade091c7f32910cb7f357ad71725017 Mon Sep 17 00:00:00 2001 From: Skander Chouchene Date: Tue, 2 Jun 2026 14:41:58 -0400 Subject: [PATCH 2/6] improve and forbid overlap --- gtfs/spec/en/reference.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gtfs/spec/en/reference.md b/gtfs/spec/en/reference.md index 0ce8704b..2a207987 100644 --- a/gtfs/spec/en/reference.md +++ b/gtfs/spec/en/reference.md @@ -549,11 +549,12 @@ For a sub-journey of two consecutive legs with a transfer, if the transfer match 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 legs are treated as one 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 legs and look at the first and last stops of the whole journey, instead of pricing leg-by-leg and defining a large number of transfer rules. +- 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 @@ -582,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.

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.

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.

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.

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.

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.

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`

If there exists a fare leg join rule 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.

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.

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`.

If there exists a fare leg join rule 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.

Valid options are:
`-1` - No limit.
`1` or more - Defines how many transfers the transfer rule may span.

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.

Conditionally Forbidden:
- **Forbidden** if `fare_transfer_rules.from_leg_group_id` does not equal `fare_transfer_rules.to_leg_group_id`.
- **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.

Must be expressed in integer increments of seconds.

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`.

Valid options are:
`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.
`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.
`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.
`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.

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.

Conditionally Required:
- **Required** if `fare_transfer_rules.duration_limit` is defined.
- **Forbidden** if `fare_transfer_rules.duration_limit` is empty. | From 35d8a22853883edf3a7e032aa7139baef7d03384 Mon Sep 17 00:00:00 2001 From: Skander Chouchene Date: Tue, 2 Jun 2026 14:44:37 -0400 Subject: [PATCH 3/6] improve wording --- gtfs/spec/en/reference.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gtfs/spec/en/reference.md b/gtfs/spec/en/reference.md index 2a207987..467aeb41 100644 --- a/gtfs/spec/en/reference.md +++ b/gtfs/spec/en/reference.md @@ -583,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.

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.

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`

If there exists a fare leg join rule 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.

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.

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`.

If there exists a fare leg join rule 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`.| +| `from_leg_group_id` | Foreign ID referencing `fare_leg_rules.leg_group_id` | Optional | Identifies a group of pre-transfer fare leg rules.

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.

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 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.

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.

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`.

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.

Valid options are:
`-1` - No limit.
`1` or more - Defines how many transfers the transfer rule may span.

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.

Conditionally Forbidden:
- **Forbidden** if `fare_transfer_rules.from_leg_group_id` does not equal `fare_transfer_rules.to_leg_group_id`.
- **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.

Must be expressed in integer increments of seconds.

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`.

Valid options are:
`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.
`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.
`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.
`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.

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.

Conditionally Required:
- **Required** if `fare_transfer_rules.duration_limit` is defined.
- **Forbidden** if `fare_transfer_rules.duration_limit` is empty. | From 88dec4d27b10301bf26fd25fb25cf6200d3c9285 Mon Sep 17 00:00:00 2001 From: Skander Chouchene Date: Wed, 8 Jul 2026 12:03:04 -0400 Subject: [PATCH 4/6] add duration limit def to fare leg join rules, transfer rules remain --- gtfs/spec/en/reference.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gtfs/spec/en/reference.md b/gtfs/spec/en/reference.md index 467aeb41..3f251842 100644 --- a/gtfs/spec/en/reference.md +++ b/gtfs/spec/en/reference.md @@ -545,6 +545,15 @@ For a sub-journey of two consecutive legs with a transfer, if the transfer match | `duration_limit_type` | Enum | **Conditionally Required** | Defines the relative start and end of `fare_leg_join_rules.duration_limit`.

Valid options are:
`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.
`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.
`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.
`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.

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.

Conditionally Required:
- **Required** if `fare_leg_join_rules.duration_limit` is defined.
- **Forbidden** if `fare_leg_join_rules.duration_limit` is empty. | +#### Calculation of `duration_limit` + +As implied by `duration_limit_type`, `duration_limit` starts and ends at fare validation events (e.g. a tap at a fare gate or onboard validator), not at the moment a rider physically boards or alights a vehicle. The `duration_limit` does not account for the time it takes a rider to cross between a fare gate and a platform. + +This means `duration_limit` should be set to the same theoretical transfer time limit that the agency publishes to riders (e.g., "transfers must be completed within 90 minutes"), rather than to a measured or estimated travel time. + +If the feed contains [pathways.txt](#pathwaystxt), data consumers can estimate this walking time using `pathways.traversal_time`. + + #### 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`. From fc6243d5da42f4bf615433b9d309675f4c4d1091 Mon Sep 17 00:00:00 2001 From: Skander Chouchene Date: Mon, 27 Jul 2026 13:25:12 -0400 Subject: [PATCH 5/6] improve paragraph --- gtfs/spec/en/reference.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/gtfs/spec/en/reference.md b/gtfs/spec/en/reference.md index 3f251842..52afb7d7 100644 --- a/gtfs/spec/en/reference.md +++ b/gtfs/spec/en/reference.md @@ -551,7 +551,9 @@ As implied by `duration_limit_type`, `duration_limit` starts and ends at fare va This means `duration_limit` should be set to the same theoretical transfer time limit that the agency publishes to riders (e.g., "transfers must be completed within 90 minutes"), rather than to a measured or estimated travel time. -If the feed contains [pathways.txt](#pathwaystxt), data consumers can estimate this walking time using `pathways.traversal_time`. +If the feed contains [pathways.txt](#pathwaystxt), data consumers should estimate the crossing time using `pathways.traversal_time`, then add these to wait times and to travel times derived from `stop_times.txt` to calculate the actual journey time for comparison against `duration_limit`. + +Alternatively, `pathways.length` or `pathways.stair_count` can be used to estimate crossing time. However, this will result in different estimations based on the assumed crossing speed. #### Using Fare Leg Join Rules or Fare Transfer Rules @@ -601,6 +603,16 @@ To process the cost of a multi-leg journey: | `fare_product_id` | Foreign ID referencing `fare_products.fare_product_id` | Optional | The fare product required to transfer between two fare legs. If empty, the cost of the transfer rule is 0.| +#### Calculation of `duration_limit` + +As implied by `duration_limit_type`, `duration_limit` starts and ends at fare validation events (e.g. a tap at a fare gate or onboard validator), not at the moment a rider physically boards or alights a vehicle. The `duration_limit` does not account for the time it takes a rider to cross between a fare gate and a platform. + +This means `duration_limit` should be set to the same theoretical transfer time limit that the agency publishes to riders (e.g., "transfers must be completed within 90 minutes"), rather than to a measured or estimated travel time. + +If the feed contains [pathways.txt](#pathwaystxt), data consumers should estimate the crossing time using `pathways.traversal_time`, then add these to wait times and to travel times derived from `stop_times.txt` to calculate the actual journey time for comparison against `duration_limit`. + +Alternatively, `pathways.length` or `pathways.stair_count` can be used to estimate crossing time. However, this will result in different estimations based on the assumed crossing speed. + ### areas.txt File: **Optional** From c0542c34d95cec7c1c883903feac6eaf0a76702e Mon Sep 17 00:00:00 2001 From: Skander Chouchene Date: Mon, 27 Jul 2026 13:52:46 -0400 Subject: [PATCH 6/6] clean paragraph --- gtfs/spec/en/reference.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gtfs/spec/en/reference.md b/gtfs/spec/en/reference.md index 52afb7d7..e3ba336f 100644 --- a/gtfs/spec/en/reference.md +++ b/gtfs/spec/en/reference.md @@ -553,7 +553,7 @@ This means `duration_limit` should be set to the same theoretical transfer time If the feed contains [pathways.txt](#pathwaystxt), data consumers should estimate the crossing time using `pathways.traversal_time`, then add these to wait times and to travel times derived from `stop_times.txt` to calculate the actual journey time for comparison against `duration_limit`. -Alternatively, `pathways.length` or `pathways.stair_count` can be used to estimate crossing time. However, this will result in different estimations based on the assumed crossing speed. +Alternatively, if `pathways.traversal_time` is not populated, `pathways.length` or `pathways.stair_count` can be used to estimate crossing time. #### Using Fare Leg Join Rules or Fare Transfer Rules @@ -611,7 +611,7 @@ This means `duration_limit` should be set to the same theoretical transfer time If the feed contains [pathways.txt](#pathwaystxt), data consumers should estimate the crossing time using `pathways.traversal_time`, then add these to wait times and to travel times derived from `stop_times.txt` to calculate the actual journey time for comparison against `duration_limit`. -Alternatively, `pathways.length` or `pathways.stair_count` can be used to estimate crossing time. However, this will result in different estimations based on the assumed crossing speed. +Alternatively, if `pathways.traversal_time` is not populated, `pathways.length` or `pathways.stair_count` can be used to estimate crossing time. ### areas.txt