Repository: graphhopper/graphhopper
Pull request #2689 (2022-10-27 17:35:42)

Add support for (single) via-way restrictions
This PR adds support for OSM restriction relations that use a single via-way (compared to the restrictions that use a via-node, that we support so far). There is an open issue for this here: #446, and @lukasalexanderweber also pushed this forward (see discussion in #2673) . Let me first explain the code in this PR. The restriction handling is done as follows:

1) When we import the OSM file we simply collect all restriction relations in a list. This is feasible as there are only about 1.7mio restriction relations for the planet file currently. We also maintain a mapping between all the OSM way IDs that are used in these restriction relations and the edges these ways are split into. Remember, each OSM way might be split into multiple graph edges during the import. So far we stored the reverse mapping (edge->way), which is easier to store because it is a 1:1 mapping, but storing the way->edge* mapping isn't a big problem either and is handled in the `WayToEdgesMap` class here. The reason I need this mapping is that I am converting each restriction relation from the 'OSM representation' (given in terms of OSM IDs) to the 'graph representation' (given in terms of edge IDs). We'll see why in a moment.

2) When the OSM import is finished we have the complete list of all the restriction relations and can now add the according turn restrictions to the graph. This can be done independently from the OSM reading process and is thus easier to maintain and test.

3) Next comes the conversion from the OSM to the graph representation. This is done in `RestrictionConverter#convert` in two steps: 
   a) We extract the relation members in the `extractMembers` function where we also print warnings about various OSM tagging mistakes (and ignore the corresponding relations)
   b) We figure out which of the (possibly many) edges that were created for the from/via/to way members are the ones that are actually part of the turn restriction to be created. This is not entirely trivial, usually OSM ways need to be split at the via members, because otherwise the restriction would be ambiguous. But the ways could still be split into multiple edges at junctions other than the junction the restriction targets. In this step we are thus also able to warn about these kinds of mapping errors and ignore the relation.
   
The previous steps can all be done independently from the vehicle type, but the following will be done separately for each vehicle type.   

4) First we parse the relation tags using one of the `RelationTagParsers` given in `OSMParsers`. This gives us the flexibility to decide which OSM tags should be considered for the turn restrictions and write different turn cost encoded values for example. The result of this parsing is basically just either `NO` (=the turn(s) corresponding to the given relation shall be restricted), `ONLY` (=all turns *except* those corresponding to the relation shall be restricted`) or `null` (=given relation shall not affect this vehicle type/relation tag parser).

5) Finally the restrictions are passed to the `RestrictionSetter` which does the actual modification of the routing graph. This can easily be done without any OSM related stuff, so we can write unit tests that just create a bare graph, define some restrictions and check if they work correctly. Basically steps 1)-4) are concerned with all the OSM parsing and step 5) is hardly related to OSM at all. I thought this is important, or at least very nice to have because we can focus on actually implementing the restrictions (which isn't that trivial, see below) without thinking about the OSM parsing in this step.

Currently there are the following limitations:

* Still no support for restriction:conditional (but this isn't really related to via-way restriction handling, either
* No support for no_entry/exit via-way restrictions with multiple from/to members. Not sure if this really is a thing, either. These kind of restrictions are already handled in steps 1)-4), though, just step 5) ignores them still. Maybe this can easily be fixed, I did not even try so far.
* No support for via-way restrictions with more than one via-way member. Again, these are already handled in steps 1)-4), but ignored in step 5) so far. *Update:* There is still a problem with multiple via-way members in step 3) (see todo in code)

Otherwise, I think this implementation is complete and I tested it visually for some restrictions like the following (I took most of these examples from #2673.

relation restriction-type, example route
2468010 no_u_turn http://localhost:8989/maps/?point=49.37924%2C8.563404&point=49.378762%2C8.562984&profile=car&layer=OpenStreetMap 222m
3567246 no_u_turn http://localhost:8989/maps/?point=49.329264%2C8.537622&point=49.329905%2C8.537087&profile=car&layer=OpenStreetMap 602m
5614305 no_u_turn http://localhost:8989/maps/?point=49.331166%2C8.536742&point=49.330506%2C8.536883&profile=car&layer=OpenStreetMap 899m
5739275 no_u_turn http://localhost:8989/maps/?point=49.298051%2C8.635106&point=49.297951%2C8.635046&profile=car&layer=OpenStreetMap 862m
7636822 only_left_turn http://localhost:8989/maps/?point=48.995991%2C8.343731&point=48.996479%2C8.345591&profile=car&layer=OpenStreetMap 192m
8409418 no_u_turn http://localhost:8989/maps/?point=48.856593%2C8.247488&point=48.856651%2C8.246936&profile=car&layer=OpenStreetMap 438m
11290869 no_u_turn http://localhost:8989/maps/?point=49.416%2C8.758752&point=49.416074%2C8.758813&profile=car&layer=OpenStreetMap 1000m
13335861 no_left_turn http://localhost:8989/maps/?point=49.002511%2C8.425032&point=49.003946%2C8.423683&profile=car&layer=OpenStreetMap 520m
13447603 no_left_turn http://localhost:8989/maps/?point=49.596021%2C8.46136&point=49.596531%2C8.462894&profile=car&layer=OpenStreetMap 355m
2602528 no_u_turn http://localhost:8989/maps/?point=49.556722%2C8.646987&point=49.556858%2C8.646931&profile=car&layer=OpenStreetMap 583m
2602528 no_u_turn http://localhost:8989/maps/?point=49.556722%2C8.646987&point=49.557818%2C8.648691&profile=car&layer=OpenStreetMap 228m
2602529 no_u_turn http://localhost:8989/maps/?point=49.556186%2C8.648994&point=49.556405%2C8.648734&profile=car&layer=OpenStreetMap 474m
2602530 no_u_turn http://localhost:8989/maps/?point=49.557452%2C8.648543&point=49.557373%2C8.648712&profile=car&layer=OpenStreetMap 401m
2602530 no_u_turn http://localhost:8989/maps/?point=49.556805%2C8.647604&point=49.556966%2C8.647838&profile=car&layer=OpenStreetMap 471m
2821562 no_u_turn http://localhost:8989/maps/?point=49.013704%2C8.35326&point=49.013462%2C8.353155&profile=car&layer=OpenStreetMap 345m
2920827 no_u_turn http://localhost:8989/maps/?point=48.995009%2C8.383025&point=48.995142%2C8.382807&profile=car&layer=OpenStreetMap 380m
2920827 no_u_turn http://localhost:8989/maps/?point=48.995474%2C8.384312&point=48.995064%2C8.382721&profile=car&layer=OpenStreetMap 148m
3399136 only_right_turn http://localhost:8989/maps/?point=49.483605%2C8.44781&point=49.482221%2C8.449843&profile=car&layer=OpenStreetMap 592m
1891360 only_left_turn http://localhost:8989/maps/?point=50.760142%2C7.022396&point=50.759247%2C7.023499&profile=car&layer=OpenStreetMap looks like a mapping error? or what does only_left_turn mean here?
10684723 only_straight_on http://localhost:8989/maps/?point=49.486035%2C7.727314&point=49.485682%2C7.726962&profile=car&layer=OpenStreetMap 401m
1300205 only_straight_on http://localhost:8989/maps/?point=53.567212%2C9.847676&point=53.567466%2C9.847521&profile=car&layer=OpenStreetMap 515m

How does the actual restriction handling work?

Via-node restrictions are handled in a similar or almost the same way as before. Maybe the only difference is that we now use a way->edge instead of an edge->way mapping. Also I no longer skip restrictions of turns that are anyway impossible, because of one-ways. I think this should be handled separately, as we might for example allow routing through one-ways in the opposite direction, but still do not like to violate turn restrictions.

Via-way restrictions are handled by adding an artificial copy of the via edge and applying turn restrictions at both ends of the via edge. It is important to:

a) only add a single artificial edge for each real edge, even when there are multiple restrictions or restrictions for different vehicles using the same via-way. 
b) restrict turns between artificial from/to edges and the artificial via-edge, because of 'overlapping' turn restrictions (see the code and tests for more details)
c) restrict turns between a real and the corresponding artificial edge

For a recent Germany OSM extract and using the car+bike profile the number of turn cost entries increased from 131291 to 166403 between master and this branch.

todo: 

* [x] Real world testing; Even for a correct implementation an open question is how many badly mapped turn restrictions there are and how this affects the overall routing quality. Update: Wrongly mapped via-way restrictions would be visible for OSRM users, so hopefully there aren't too many. I ran 75k random queries in Germany to check for connection not found errors. There were only 22 and all of them were due to isolated subnetworks (I used a rather small value of 200). Actually almost all of them included a query point that was either in Volkswagen Prüfgelände Ehra-Lessien or Ziegelrodaer Forst.
* [x] I am not entirely sure what 'only' via-way restrictions are even supposed to mean, like what should be allowed and what shouldn't. Need to look at more real-world example to make sure the current implementation interprets these restrictions in a useful way.
* [x] Clean up and add more tests
* [ ] Support multi-via-way restrictions (probably in a follow-up PR)
* [ ] Support multi-from/to-way via restrictions (but check if they are really used and if not maybe stop considering them in steps 1)-4).
* [x] Do we need to explicitly prevent snapping to artificial edges? -> Apparently not, because the LocationIndex will return the first edge that was added to the graph when there are multiple identical edges, so it will always return the original edge instead of the artificial one automatically.
* [x] Make sure that post-import modifications of the edge properties are also applied to the corresponding artificial edges. Update: So far we rely on the post-import modifications to take care of this themselves.

easbar: Btw here is the list of turn restriction warnings printed with this PR for the current OSM file. I'll update this from time to time:

https://gist.github.com/easbar/66446066ba7a129e4a643bde3fbd12c6

There are about 33000 such warnings and about 25000 of these are due to missing from/via/to members. In many cases these restrictions got broken when the member ways/nodes were edited after the restrictions were created.

I opened an issue in iD for this: https://github.com/openstreetmap/iD/issues/9346
lukasalexanderweber: Thanks for including the comments into your code! 

> Btw here is the list of turn restriction warnings printed with this PR for the current OSM file. I'll update this from time to time:

Maybe for more real world testing it would be nice to have another gist with all working via way restrictions? It would be helpful for e.g. 

> I am not entirely sure what 'only' via-way restrictions are even supposed to mean, like what should be allowed and what shouldn't. Need to look at more real-world example to make sure the current implementation interprets these restrictions in a useful way.

or 

> Support multi-from/to-way via restrictions (but check if they are really used and if not maybe stop considering them in steps 1)-4).

or having a closer look on multi via way examples.

> _Update:_ There is still a problem with multiple via-way members in step 3) (see todo in code)

Should I have a look on that? Or are there other todos I can help with?



easbar: > Maybe for more real world testing it would be nice to have another gist with all working via way restrictions? It would be helpful for e.g.

Yes, I think it would be helpful to have some kind of corpus of turn restriction cases that we can test the implementation against (now but also in the future). Like the list of examples you started in one of your comments, and that I continued above with explicit links to some routes with the expected distance (assuming the restriction works correctly), but something that is more organized and that we can run automatically.

So far my workflow was something like this: 

a) Run the OSM import and print some logs to find certain kinds of turn restrictions, 
b) look them up on openstreetmap.org/relation/123, which worked because I was using recent OSM data, 
c) start GH and see if the routes work. 

This repository already contains a relatively large OSM file for the city of Leipzig: https://github.com/graphhopper/graphhopper/blob/master/map-matching/files/leipzig_germany.osm.pbf
We could check if this contains enough 'interesting' turn restrictions and write some JUnit tests against this with the advantage that the file is already there. The annoying part might be that the restrictions cannot easily be visualized on openstreetmap.org, because the file is rather old. But even if it was more recent the situation would be the same in a year or so. Offline visualization could work with JOSM, though (I did not try this yet).

An alternative could be searching for interesting turn restrictions and then extracting mini-OSM file using the 'export' functionality on openstreetmap.org for example.

Then again, it should be sufficient to simply create artificial restriction situations (maybe resembling some we found in the wild) in RestrictionSetterTest. These will be easier to debug anyway and as long as the 'conversion' code works correctly this is the part that is more interesting to test anyway. Maybe the goal should be to have only a few tests that use real OSM data and include the full OSM import, and in turn more such low level tests.

But maybe first of all we should just continue our list of 'interesting' turn restrictions manually and sort it a bit. I'd be interested in  'only' via-way restrictions specifically, because so far I wasn't entirely sure how they should be treated. We could use our import to print a list of these (or the same for some other kind like multi-via way), or figure out a query that does the same on overpass-turbo.eu (I tried this briefly, but didn't have the patience to study the query language).

> or having a closer look on multi via way examples. Should I have a look on that?

Yes, of course if you want to push this forward please go ahead. In case I find more time to work on this PR I will rather focus on finishing the single via-way case. Note that there is still an issue for multi-via ways even in step 3). To fix this, maybe we can make the assumption that via-ways always have only two tower nodes (for these this should actually be the case and we can warn/ignore otherwise)?

> Or are there other todos I can help with?

Right now I think a list of 'interesting' via-way restrictions of type 'only' would be quite useful. Also to make sure there aren't (many) cases where handling the via-way turn restrictions makes the routing **worse**. For example this could be the case if restrictions were mapped incorrectly (or there is a bug on our side).
lukasalexanderweber: Ok, so why not specify requirements on a Via Way Turn Restriction dataset which we can filter for some representative examples:

We would need
- Id
- number from ways
- number via ways
- number to ways
- restriction type

have you any other requirements?

> Note that there is still an issue for multi-via ways even in step 3)

> _Update:_ There is still a problem with multiple via-way members in step 3) (see todo in code)


I don't find the inline comment you mean, could you give me a hint?



easbar: > I don't find the inline comment you mean, could you give me a hint?

here: https://github.com/graphhopper/graphhopper/pull/2689/commits/77a939a7396c0211b4b0570faaf736d1a3c71420
Do you see the problem? But if we assume every via way corresponds to exactly one edge (which we can easily check and warn/ignore otherwise) this should be a bit easier.

> have you any other requirements?

No, your list looks like a good start. And maybe keep track of the OSM file with timestamp you used.
lukasalexanderweber: > here: [77a939a](https://github.com/graphhopper/graphhopper/commit/77a939a7396c0211b4b0570faaf736d1a3c71420)
> Do you see the problem? But if we assume every via way corresponds to exactly one edge (which we can easily check and warn/ignore otherwise) this should be a bit easier.

I see the problem, but I don't understand if this is "only" a problem of finding the correct order of the via ways or what this has to do with tower nodes / 1:1 mapping.
easbar: > I see the problem, but I don't understand if this is "only" a problem of finding the correct order of the via ways or what this has to do with tower nodes / 1:1 mapping.

I just thought it could be easier if we can assume a 1:1 mapping, but probably it doesn't really make a difference. However, I think if there are multiple edges for a via way we should stop/warn anyway. I think I fixed this problem here: 8729462dc341cb272875873a4124757e677951e9
lukasalexanderweber: Ok, just that we are REALY on the same page:

In https://github.com/graphhopper/graphhopper/pull/2673 we had this conversation: 

> > One non-trivial issue was that there is no 1:1 mapping between the OSM way members of the restriction relations and the edges we add to the routing graph. I think you simply excluded these cases so far, but I wrote code that handles these as well.
> 
> Yeah until then I had assumed that all (via) ways have only two tower nodes (for me those are the edges that really have a 1:1 mapping to the OSM Ways). Example Restrictions with more than two tower nodes are `6315945` and `10164406`, which are both managed correctly by your implementation

So this is not clear to me:
- In the two examples, we can see that both times the from way is splitted into multiple edges. The restriction is handled correctly by your approach. What would happen if the via way is splitted into multiple edges? Would it stop | warn | handle it correctly? 
- If it's handled correctly, why should't it work for multi via ways as well?

Thank you for the fix! For me the [WayToEdgeConverter](https://github.com/graphhopper/graphhopper/blob/8729462dc341cb272875873a4124757e677951e9/core/src/main/java/com/graphhopper/reader/osm/WayToEdgeConverter.java) is one of the hardest things to understand in your approach since I'm not familiar with DFS, backtracking, EdgeIteratorState and (Java) Iterators. I try to think myself in but it takes some time. 

Anyway, it's great to see in this commit how easy unit tests are set up by splitting restriction handling from osm reading 👍 
easbar: > Yeah until then I had assumed that all (via) ways have only two tower nodes (for me those are the edges that really have a 1:1 mapping to the OSM Ways). Example Restrictions with more than two tower nodes are 6315945 and 10164406, 

But these are examples for restrictions where the from- (not via-) member has multiple tower nodes, right?

> In the two examples, we can see that both times the from way is splitted into multiple edges. The restriction is handled correctly by your approach. What would happen if the via way is splitted into multiple edges? Would it stop | warn | handle it correctly?

Yes, the from way is split into multiple edges (because in each case it is crossed by another road). If the via way was crossed by another road (and thus be split into multiple edges) the restriction currently would be ignored and there would be a warning. I just added a test for this here: e2120d160e0b18282b4fb996f2008756a3d561f6.

> If it's handled correctly, why should't it work for multi via ways as well?

It does work for multi via ways as well. I mean we get a warning and ignore the restriction whenever a via way is split into multiple edges no matter how many via ways there are.

> For me the [WayToEdgeConverter](https://github.com/graphhopper/graphhopper/blob/8729462dc341cb272875873a4124757e677951e9/core/src/main/java/com/graphhopper/reader/osm/WayToEdgeConverter.java) is one of the hardest things to understand in your approach since I'm not familiar with DFS, backtracking, EdgeIteratorState and (Java) Iterators. I try to think myself in but it takes some time.

Yes, I probably wouldn't understand this either without understanding what it is supposed to do first and then trying to implement it myself. And maybe there is an easier solution I did not consider yet. Let's say there were 20 via ways and we don't know their order. But we need to know the order and the nodes that connect the corresponding edges to setup the right turn restrictions. There would be 20! (very very many) possibilities to sort the via ways so we cannot just brute force try all the combinations. What we do instead is pick one via way and check if it is connected to the from-way-edge. If it isn't connected we can stop the search at this point (take one step back) and try another edge. Otherwise we pick one of the remaining edges and see if it is connected to the previous one and so on.
lukasalexanderweber: Here is a [gist](https://gist.github.com/lukasalexanderweber/e4c0b9106592ad7452aecc203cbb78ad) with the via way restrictions in germany. I havn't found via way restrictions with multiple from/to ways (or my code is wrong lol).

We have 2533 via way restrictions, 150 of them are multi via way restrictions. 
We have 201 single via way ONLY restrictions and 18 multi via way ONLY restrictions.
lukasalexanderweber: > But these are examples for restrictions where the from- (not via-) member has multiple tower nodes, right?

Right



> If the via way was crossed by another road (and thus be split into multiple edges) the restriction currently would be ignored and there would be a warning [...] It does work for multi via ways as well.

Thanks for making this clear and adding the test!

> What we do instead is pick one via way and check if it is connected to the from-way-edge. If it isn't connected we can stop the search at this point (take one step back) and try another edge. Otherwise we pick one of the remaining edges and see if it is connected to the previous one and so on.

Nice, thanks for pointing it out!
lukasalexanderweber: For the ToDo "Clean up":

Maybe we should rename `RestrictionMembers` to `OSMRestriction`.

So we have `OSMRestriction` on the one hand and `GraphRestriction` on the other, making this pretty clear.

We could then have two main functions in the `RestrictionConverter` (or split it into two classes )

- `relation2osmRestriction(relation)`
- `osmRestriction2graphRestriction(osmRestriction, baseGraph, waysToEdgesMap)`

till now, `RestrictionConverter` handles most of the `relation2osmRestriction()` stuff, while `WayToEdgeConverter` handles most of `osmRestriction2graphRestriction()`. So maybe we can also get rid of the name `WayToEdgeConverter`, which I think is not as clear as it could be.
lukasalexanderweber: I tested some other ONLY via way restrictions: 

these are working fine
[4474926](https://www.openstreetmap.org/relation/4474926): [new](http://localhost:8989/maps/?point=51.477283%2C6.955486&point=51.476999%2C6.955196&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=51.47722%2C6.95532%3B51.47701%2C6.95524)
[4489203](https://www.openstreetmap.org/relation/4489203): [new](http://localhost:8989/maps/?point=53.058415%2C8.970956&point=53.058299%2C8.970961&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=53.05841%2C8.97095%3B53.05829%2C8.97095#map=19/53.05834/8.97084)
[4857636](https://www.openstreetmap.org/relation/4857636): [new](http://localhost:8989/maps/?point=52.367076%2C13.422795&point=52.366923%2C13.422798&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=52.3671%2C13.4234%3B52.3670%2C13.4235#map=19/52.36702/13.42287)
[5412428](https://www.openstreetmap.org/relation/5412428): [new](http://localhost:8989/maps/?point=54.299483%2C13.093613&point=54.29929%2C13.093715&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=54.29948%2C13.09364%3B54.29931%2C13.09384#map=18/54.29926/13.09360) + 5412429
[5551153](https://www.openstreetmap.org/relation/5551153): [new](http://localhost:8989/maps/?point=52.812573%2C8.212494&point=52.81235%2C8.212345&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=52.81254%2C8.21242%3B52.81235%2C8.21230#map=19/52.81213/8.21282) + 5551157 + 5551158
[6412742](https://www.openstreetmap.org/relation/6412742): [new](http://localhost:8989/maps/?point=52.346361%2C13.28438&point=52.350097%2C13.288715&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=52.3459%2C13.2843%3B52.3501%2C13.2887#map=16/52.3480/13.2865)
[6693037](https://www.openstreetmap.org/relation/6693037): [new](http://localhost:8989/maps/?point=48.787835%2C8.979964&point=48.787446%2C8.981166&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=48.78778%2C8.98006%3B48.78743%2C8.98121#map=19/48.78760/8.98064)
[6799370](https://www.openstreetmap.org/relation/6799370): [new](http://localhost:8989/maps/?point=52.538483%2C13.326452&point=52.538281%2C13.325679&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=52.53844%2C13.32633%3B52.53830%2C13.32576#map=19/52.53837/13.32604)

this one is not working (it's in your gist, but in OSRM it works):
[5709253](https://www.openstreetmap.org/relation/5709253): [new](http://localhost:8989/maps/?point=49.736792%2C6.613201&point=49.737101%2C6.613271&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=49.73671%2C6.61310%3B49.73710%2C6.61325#map=19/49.73697/6.61323)

there are some "senseless" restrictions, mainly because they are double informations with one ways. But most importantly they don't "destroy" the routing:
[4498351](https://www.openstreetmap.org/relation/4498351): [new](http://localhost:8989/maps/?point=50.072186%2C8.247976&point=50.071831%2C8.249246&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=50.07226%2C8.24794%3B50.07182%2C8.24923#map=19/50.07202/8.24860)
[6627992](https://www.openstreetmap.org/relation/6627992): [new](http://localhost:8989/maps/?point=48.690275%2C9.184827&point=48.69049%2C9.184563&profile=car&layer=OpenStreetMap)

Example for via ways having more than two tower nodes (not in your gist yet):
[4500343](https://www.openstreetmap.org/relation/4500343#map=19/50.07187/8.24838)
easbar: > Here is a [gist](https://gist.github.com/lukasalexanderweber/e4c0b9106592ad7452aecc203cbb78ad) with the via way restrictions in germany.

Thanks!

> We have 201 single via way ONLY restrictions 

How many of them did you check manually yet? Maybe we should check around 50 or so total?

> Maybe we should rename RestrictionMembers to OSMRestriction.

Not sure if this would be so helpful, because RestrictionMembers does not contain any information about the actual restriction tags. Regarding the 'clean up' todo I was mostly referring to the tests, but if you have suggestions about the code I'll consider them as well of course.

> So maybe we can also get rid of the name WayToEdgeConverter, which I think is not as clear as it could be.

Ok, then again it is only some kind of internal piece of code related to the conversion.

> this one is not working (it's in your gist, but in OSRM it works):
[5709253](https://www.openstreetmap.org/relation/5709253): [new](http://localhost:8989/maps/?point=49.736792%2C6.613201&point=49.737101%2C6.613271&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=49.73671%2C6.61310%3B49.73710%2C6.61325#map=19/49.73697/6.61323)

Ah yes, for this one we currently get this warning:
```
Restriction relation 5709253 has via member way that isn't split at adjacent ways: 739453982. tags: {restriction=only_straight_on, type=restriction}, members: [to way 739453984, via way 739453982, from way 739453981]. Relation ignored.
```

and this is because the via-way member is crossed by this path:

![image](https://user-images.githubusercontent.com/17603532/200163290-49913fd5-cb6a-4ce4-9b0c-816bd0aa1ab8.png)

We could try to recognize this somehow, but I think I'd rather treat this as a mapping error, at least for now. The OSM wiki is not clear about this?!


lukasalexanderweber: > How many of them did you check manually yet? Maybe we should check around 50 or so total?

I checked the ~15 shown in the last comment. I'll check another 35.



> So I think I'd rather treat this as a mapping error, at least for now.

Yeah I think so, too. Support for those could still be added in a subsequent PR.
easbar: > I checked the ~15 shown in the last comment. I'll check another 35.

Ok thanks! Did you check them in any order? I could start checking a few starting with the highest ids in decreasing order. I'm mostly interested in figuring out how the via-way 'only' restrictions are meant to be interpreted. For example take a look at this example:

```
 a   |b    c       from=a,via=d,to=e
-----|------
     |d
-----|------
 e   |f    g   
```

A 'no' restriction seems pretty clear: Basically every turn is allowed except turning from d to e **if** we came from a. Turning from a to d and continuing straight to f is allowed, and so is turning from d to e when we came from b or c.

But what would it mean if this was an 'only' restriction? Does it mean coming from a we can **only** turn from a to d and not from a to b or a to c? And similar does it mean we can only turn from d to e, but not from d to f or d to g? Or does it mean we can only turn from d to e **if** we came from a? And if we came from a, does it mean we have to turn to d and then e?

So far this is not clear to me and since this isn't really explained in the wiki (?!) I'm afraid these kind of restrictions aren't used consistently in OSM? So by 'checking manually' I thought we should figure out if all the cases we find seem to follow the same conventions. Are there examples for different kinds of definitions/interpretations of such restrictions?

> Yeah I think so, too. Support for those could still be added in a subsequent PR.

Ok.

easbar: Maybe we can also take a look at how many 'only' via-way restrictions there are for the different restriction values. Are there mostly 'only_straight_on' restrictions for example?
lukasalexanderweber: > Ok thanks! Did you check them in any order?



> [4474926](https://www.openstreetmap.org/relation/4474926): [new](http://localhost:8989/maps/?point=51.477283%2C6.955486&point=51.476999%2C6.955196&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=51.47722%2C6.95532%3B51.47701%2C6.95524)
> [4489203](https://www.openstreetmap.org/relation/4489203): [new](http://localhost:8989/maps/?point=53.058415%2C8.970956&point=53.058299%2C8.970961&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=53.05841%2C8.97095%3B53.05829%2C8.97095#map=19/53.05834/8.97084)
> [4857636](https://www.openstreetmap.org/relation/4857636): [new](http://localhost:8989/maps/?point=52.367076%2C13.422795&point=52.366923%2C13.422798&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=52.3671%2C13.4234%3B52.3670%2C13.4235#map=19/52.36702/13.42287)
> [5412428](https://www.openstreetmap.org/relation/5412428): [new](http://localhost:8989/maps/?point=54.299483%2C13.093613&point=54.29929%2C13.093715&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=54.29948%2C13.09364%3B54.29931%2C13.09384#map=18/54.29926/13.09360) + [5412429](https://www.openstreetmap.org/relation/5412429)
> [5551153](https://www.openstreetmap.org/relation/5551153): [new](http://localhost:8989/maps/?point=52.812573%2C8.212494&point=52.81235%2C8.212345&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=52.81254%2C8.21242%3B52.81235%2C8.21230#map=19/52.81213/8.21282) + [5551157](https://www.openstreetmap.org/relation/5551157)+ [5551158](https://www.openstreetmap.org/relation/5551158)
> [6412742](https://www.openstreetmap.org/relation/6412742): [new](http://localhost:8989/maps/?point=52.346361%2C13.28438&point=52.350097%2C13.288715&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=52.3459%2C13.2843%3B52.3501%2C13.2887#map=16/52.3480/13.2865)
> [6693037](https://www.openstreetmap.org/relation/6693037): [new](http://localhost:8989/maps/?point=48.787835%2C8.979964&point=48.787446%2C8.981166&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=48.78778%2C8.98006%3B48.78743%2C8.98121#map=19/48.78760/8.98064)
> [6799370](https://www.openstreetmap.org/relation/6799370): [new](http://localhost:8989/maps/?point=52.538483%2C13.326452&point=52.538281%2C13.325679&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=52.53844%2C13.32633%3B52.53830%2C13.32576#map=19/52.53837/13.32604)

I checked the range from 4474926 to 6799370. 

> I could start checking a few starting with the highest ids in decreasing order

So yes, you can do that!, i'll do some more "lower" ids.

> But what would it mean if this was an 'only' restriction? Does it mean coming from a we can **only** turn from a to d and not from a to b or a to c? And similar does it mean we can only turn from d to e, but not from d to f or d to g? Or does it mean we can only turn from d to e **if** we came from a? And if we came from a, does it mean we have to turn to d and then e?

1. Does it mean coming from a we can **only** turn from a to d and not from a to b or a to c: **YES**
2. And similar does it mean we can only turn from d to e, but not from d to f or d to g? **NO** (Why should it? A simple node restriction would then be enough)
3. Or does it mean we can only turn from d to e if we came from a? -> I didn't find those cases
4. And if we came from a, does it mean we have to turn to d and then e? -> This is the same as 1., isn't it? So **YES**
lukasalexanderweber: > But nice to hear that apparently the restrictions you saw seem to be consistent then? And did we implement the restrictions in a way that makes sense to you?

Yeah definitely. If you look into the examples it really makes sense :)
lukasalexanderweber: > Maybe we can also take a look at how many 'only' via-way restrictions there are for the different restriction values. Are there mostly 'only_straight_on' restrictions for example?

the 201 single way ONLY restrictions split into

114 `only_straight_on`
72 `only_left_turn`
15 `only_right_turn`
lukasalexanderweber: Ok I've tested another 30:

`1027521, 1161597, 1214431, 1300205, 1308373, 1458863, 1469894, 1469898, 1526354, 1531678, 1607620, 1631805, 1671549, 1782600, 1849397, 1850252, 1891360, 1974644, 2045135, 2045143, 2117158, 2216966, 2293029, 2293161, 2293165, 2304336, 2307767, 2343998, 2361774, 2382429
`

Summary:
- 1 **critical error** where the restriction handling leads to a broken geometry: [2304336](https://www.openstreetmap.org/relation/2304336#map=18/49.97636/7.91236): [new](http://localhost:8989/maps/?point=49.976763%2C7.914409&point=49.976604%2C7.914427&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=49.97677%2C7.91440%3B49.97659%2C7.91447)
- All other restrictions work perfectly fine as we're expecting
- 2 restrictions are working with our approach, while apperently not being recognized by OSRM (not really important for us but maybe worth a mention):
  -  [2293161](https://www.openstreetmap.org/relation/2293161): [new](http://localhost:8989/maps/?point=50.921801%2C6.966407&point=50.922315%2C6.967067&profile=car&layer=OpenStreetMap) vs [osrm](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=50.92180%2C6.96593%3B50.92210%2C6.96718)
  -  [1891360](https://www.openstreetmap.org/relation/1891360): [new](http://localhost:8989/maps/?point=50.760782%2C7.021667&point=50.759299%2C7.021565&profile=car&layer=OpenStreetMap) vs [osrm](https://www.openstreetmap.org/directions?engine=fossgis_osrm_car&route=50.76079%2C7.02161%3B50.75956%2C7.02213)
easbar: > Ok I've tested another 30:
> the 201 single via way ONLY restrictions split into

Thanks a lot for this!

> Yeah definitely. If you look into the examples it really makes sense :)

So our current definitions should be ok?!

https://github.com/graphhopper/graphhopper/blob/69799fa14bafd6c6f185d1738c67eb5bc25f54d3/core/src/main/java/com/graphhopper/routing/util/parsers/RestrictionSetter.java#L114-L115

> 1 critical error where the restriction handling leads to a broken geometry: [2304336](https://www.openstreetmap.org/relation/2304336#map=18/49.97636/7.91236): [new](http://localhost:8989/maps/?point=49.976763%2C7.914409&point=49.976604%2C7.914427&profile=car&layer=OpenStreetMap) vs [old](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=49.97677%2C7.91440%3B49.97659%2C7.91447)

This seems like a bug on our side and due to two only via-way restrictions sharing the same via way. I added a failing test here: 69799fa14bafd6c6f185d1738c67eb5bc25f54d3, but did not think about or try to fix it yet.

> [2293161](https://www.openstreetmap.org/relation/2293161): [new](http://localhost:8989/maps/?point=50.921801%2C6.966407&point=50.922315%2C6.967067&profile=car&layer=OpenStreetMap) vs [osrm](https://www.openstreetmap.org/directions?engine=graphhopper_car&route=50.92180%2C6.96593%3B50.92210%2C6.96718)

Here I get this route: 
![image](https://user-images.githubusercontent.com/17603532/200396880-681cdec1-70d2-47d7-8421-ac962acd0475.png)
Not really what we want and the same as OSRM (the restriction is ignored?)

> [1891360](https://www.openstreetmap.org/relation/1891360): [new](http://localhost:8989/maps/?point=50.760782%2C7.021667&point=50.759299%2C7.021565&profile=car&layer=OpenStreetMap) vs [osrm](https://www.openstreetmap.org/directions?engine=fossgis_osrm_car&route=50.76079%2C7.02161%3B50.75956%2C7.02213)

This one seems a bit strange. First of all why is it even there? Looking at the satellite imagery we can clearly see that turning right is allowed? And why does OSRM not recognize this one? It doesn't seem to be so special? Or maybe, because it overlaps with this 'no u-turn' via-way restriction: https://www.openstreetmap.org/relation/1891361 ? But anyway not so important for this PR probably.
lukasalexanderweber: > So our current definitions should be ok?!
> 
> https://github.com/graphhopper/graphhopper/blob/69799fa14bafd6c6f185d1738c67eb5bc25f54d3/core/src/main/java/com/graphhopper/routing/util/parsers/RestrictionSetter.java#L114-L115
> 
> >

Yes, this is on point.

> I added a failing test here: [69799fa](https://github.com/graphhopper/graphhopper/commit/69799fa14bafd6c6f185d1738c67eb5bc25f54d3)

Thanks for analyzing and adding the test! This case could maybe need some more logic as we store now: 

1. store ONLY restrictions sharing the same via way
2. handle the turn costs differently

https://github.com/graphhopper/graphhopper/blob/69799fa14bafd6c6f185d1738c67eb5bc25f54d3/core/src/main/java/com/graphhopper/routing/util/parsers/RestrictionSetter.java#L124

here we could allow multiple toEdges 

> Not really what we want and the same as OSRM (the restriction is ignored?)
> Update: The reason the restriction was ignored on my machine is probably that I use a car and a foot profile. In this case the [via way](https://www.openstreetmap.org/way/7990007) is crossed by this [footway](https://www.openstreetmap.org/way/927714594), so the same kind of situation as with 5709253 that we discussed above.

Ahh it's nice for me to see that in some cases the correct handling of turn restrictions is dependent on the profiles. I only use car, so the footway is filtered out and the restriction is correct (yes 5709253 was the same but I didn't recognize back then that this is dynamic):

![image](https://user-images.githubusercontent.com/32765578/200496240-4b0fffd7-2d3a-48c6-b215-ac328a16af0c.png)

> This one seems a bit strange. First of all why is it even there?

i was wondering the exact same thing xD

> But anyway not so important for this PR probably

yes, I think so, too
easbar: > Thanks for analyzing and adding the test! This case could maybe need some more logic as we store now:

Unfortunately, yes. But we could also try to ignore such cases for now. The restriction is valid though:

![image](https://user-images.githubusercontent.com/17603532/200504069-b06b518b-fc62-4313-881b-8ee6f6b99146.png)

Drivers coming from the northern lane (yellow) are supposed to go straight while those coming from the southern lane (red) are supposed to turn left (and enter the ferry). OSRM seems to recognize this, but then it suggests a u-turn to re-enter from the other lane, that is also violating the restriction:

![image](https://user-images.githubusercontent.com/17603532/200504447-ace305cb-fdc5-4401-b0ae-9345354c902f.png)
![image](https://user-images.githubusercontent.com/17603532/200504513-7ea6a8f6-adc6-4c2a-9ce9-b892e17c9426.png)

> But anyway not so important for this PR probably

There will be cases of such wrongly mapped turn restrictions that were ignored (by GraphHopper) so far and once we use them the routing will go 'wrong' (but be correct according to OSM). But these should be rare and also help improving the map.



lukasalexanderweber: can you have a look at https://github.com/lukasalexanderweber/graphhopper_patch/commit/d97fa75c237d4d301744a245a6e3a377cff25312, this should be a valid way to solve the problem (tests are passing)
easbar: > here we could allow multiple toEdges

Yes, but to really handle this case correctly I think we would need a second artificial edge, because once we turn onto 'the' via edge we no longer 'know' whether we came from the northern or southern from way. And we can only distinguish these by using more artificial via edges. But if it is only for a few such special cases I would much rather ignore these kind of restrictions.

Did you already find out if we will be able to handle multi via-way restrictions with our current approach of storing at most one artificial edge per edge?
easbar: > can you have a look at https://github.com/lukasalexanderweber/graphhopper_patch/commit/d97fa75c237d4d301744a245a6e3a377cff25312, this should be a valid way to solve the problem (tests are passing)

Thanks, yes I tried. Unfortunately if I add some more checks the test fails: 9efec9aba092b237d33c4702deed5f98e3bc0148
And this is what I mentioned above: If we allow more to-edges we no longer enforce the restrictions properly. Like in the Rüdesheim example we could go straight even though we were on the 'turn left to enter the ferry'-lane. So not sure if this is better than simply ignoring the entire restriction?
lukasalexanderweber: > can you have a look at [lukasalexanderweber/graphhopper_patch@d97fa75](https://github.com/lukasalexanderweber/graphhopper_patch/commit/d97fa75c237d4d301744a245a6e3a377cff25312), this should be a valid way to solve the problem (tests are passing)

with my actual config it detours over the ferry to make the u-turn xD
![image](https://user-images.githubusercontent.com/32765578/200509530-785b63d1-5fd2-4475-828e-b5ae54092eda.png)

> But if it is only for a few such special cases I would much rather ignore these kind of restrictions.

Yes we can also do that. We could filter them out with a similar for loop.







> Did you already find out if we will be able to handle multi via-way restrictions with our current approach of storing at most one artificial edge per edge?

I tested it back then with my implementation (https://github.com/graphhopper/graphhopper/pull/2673#issuecomment-1285588483) and I can't think of any reason why it shouldn't with yours, too
lukasalexanderweber: > So not sure if this is better than simply ignoring the entire restriction?

Yes we should probably ignore this edge case. But it's important that we really ignore them. Will you implement it or should I give it a shot?
easbar: This should do it: 0f8dfb7fd239d4fbb7ee4ba11110af4a250c99ee
It accepts the first restriction and ignores all others with the same via edge. Or shall we ignore all restrictions that share the same via edge?
easbar: > with my actual config it detours over the ferry to make the u-turn xD

Oh, haha :D But at least that's correct according to the restrictions.
lukasalexanderweber: shouldn't we log a warning or something? Since it's a valid restriction this might be appropriate.

> Or shall we ignore all restrictions that share the same via edge?

We should probably support as much as we can with the current implementation, so I like it as it is
easbar: > shouldn't we log a warning or something? Since it's a valid restriction this might be appropriate.

Ok, will do.
easbar: Via-way 'only' restrictions are now ignored when a via-way is used a second time by such a restriction and we print a warning in this case. For Germany there were only four such cases. Relations: 2729795, 3498481, 10398564 and 13716158.
easbar: I checked another ~20 via-way 'only' restrictions (from bottom to top of your list). Looks good. I think this is ready to merge.
easbar: @lukasalexanderweber I added you to the contributors list, thanks a lot for your help!
easbar: Unfortunately, there seems to be some kind of snapping or access issue still: 

This car wash can neither be used by cars nor is it excluded from the network: http://localhost:8989/maps/?point=52.508133%2C13.2807&point=52.507974%2C13.280731&profile=car&layer=OpenStreetMap
easbar: ... but there is another thing that is still missing: We need to keep the `artificialEdgesByEdges` map for the post-import steps where the edges might still be modified and make sure that the artificial edges are modified in the same way.

Patch:
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 11a77144e..94723a47d 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -48,7 +48,7 @@ Here is an overview:
  * kodonnell, adding support for CH and other algorithms (#60) and penalizing inner-link U-turns (#88)
  * legraina, improved docker for dockerhub
  * lmar, improved instructions
- * lukasalexanderweber, fixes like #2652
+ * lukasalexanderweber, helped to implement via-way restrictions #2689 and fixes like #2652
  * matkoniecz, tweaking documentation
  * manueltimita, fixes like #1651
  * mathstpierre, fixes like #1753
diff --git a/core/src/main/java/com/graphhopper/GraphHopper.java b/core/src/main/java/com/graphhopper/GraphHopper.java
index edde427a8..2b7f010a8 100644
--- a/core/src/main/java/com/graphhopper/GraphHopper.java
+++ b/core/src/main/java/com/graphhopper/GraphHopper.java
@@ -820,6 +820,10 @@ public class GraphHopper {
     }
 
     protected void postImport() {
+        // Important note: To deal with via-way turn restrictions we introduce artificial edges in OSMReader (#2689).
+        // These are simply copies of real edges. Any further modifications of the graph edges must take care of keeping
+        // the artificial edges in sync with their real counterparts. So if an edge attribute shall be changed this change
+        // must also be applied to the corresponding artificial edge.
         if (sortGraph) {
             BaseGraph newGraph = GHUtility.newGraph(baseGraph);
             GHUtility.sortDFS(baseGraph, newGraph);
diff --git a/core/src/main/java/com/graphhopper/reader/ReaderElement.java b/core/src/main/java/com/graphhopper/reader/ReaderElement.java
index 14399bd06..3784f0811 100644
--- a/core/src/main/java/com/graphhopper/reader/ReaderElement.java
+++ b/core/src/main/java/com/graphhopper/reader/ReaderElement.java
@@ -28,7 +28,7 @@ import java.util.Map.Entry;
  * @author Peter
  */
 public abstract class ReaderElement {
-    public static enum Type {
+    public enum Type {
         NODE,
         WAY,
         RELATION,
diff --git a/core/src/main/java/com/graphhopper/reader/osm/GraphRestriction.java b/core/src/main/java/com/graphhopper/reader/osm/GraphRestriction.java
new file mode 100644
index 000000000..81f252620
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/reader/osm/GraphRestriction.java
@@ -0,0 +1,101 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+import com.carrotsearch.hppc.IntArrayList;
+
+/**
+ * Basically an OSM restriction, but in 'graph-representation', i.e. it is expressed in terms of graph node/edge IDs
+ * instead of OSM way IDs. There can be via-node-restrictions (with a single via-node) and via-way/edge-restrictions
+ * (with one or more via-edges). There can also be multiple from- or to-edges to represent OSM restrictions like
+ * no_entry or no_exit that use multiple from- or to-members.
+ * <p>
+ * We store a list of via-nodes even for via-way restrictions. It stores the nodes connecting the via-ways,
+ * see {@link WayToEdgeConverter.EdgeResult}. For via-node restrictions the list simply contains the single via node.
+ * <p>
+ * This class only contains the 'topology' of the restriction. The {@link RestrictionType} is handled separately,
+ * because opposite to the type the topology does not depend on the vehicle type.
+ */
+public class GraphRestriction {
+    private final boolean isViaWayRestriction;
+    private final IntArrayList viaNodes;
+    private final IntArrayList fromEdges;
+    private final IntArrayList viaEdges;
+    private final IntArrayList toEdges;
+
+    public static GraphRestriction node(int fromEdge, int viaNode, int toEdge) {
+        return node(IntArrayList.from(fromEdge), viaNode, IntArrayList.from(toEdge));
+    }
+
+    public static GraphRestriction node(IntArrayList fromEdges, int viaNode, IntArrayList toEdges) {
+        return new GraphRestriction(false, IntArrayList.from(viaNode), fromEdges, null, toEdges);
+    }
+
+    public static GraphRestriction way(int fromEdge, int viaEdge, int toEdge, IntArrayList viaNodes) {
+        return way(fromEdge, IntArrayList.from(viaEdge), toEdge, viaNodes);
+    }
+
+    public static GraphRestriction way(int fromEdge, IntArrayList viaEdges, int toEdge, IntArrayList viaNodes) {
+        return way(IntArrayList.from(fromEdge), viaEdges, IntArrayList.from(toEdge), viaNodes);
+    }
+
+    public static GraphRestriction way(IntArrayList fromEdges, IntArrayList viaEdges, IntArrayList toEdges, IntArrayList viaNodes) {
+        return new GraphRestriction(true, viaNodes, fromEdges, viaEdges, toEdges);
+    }
+
+    private GraphRestriction(boolean isViaWayRestriction, IntArrayList viaNodes, IntArrayList fromEdges, IntArrayList viaEdges, IntArrayList toEdges) {
+        if (fromEdges.size() > 1 && toEdges.size() > 1)
+            throw new IllegalArgumentException("fromEdges and toEdges cannot be size > 1 at the same time");
+        if (fromEdges.isEmpty() || toEdges.isEmpty())
+            throw new IllegalArgumentException("fromEdges and toEdges must not be empty");
+        if (!isViaWayRestriction && viaNodes.size() != 1)
+            throw new IllegalArgumentException("for node restrictions there must be exactly one via node");
+        if (!isViaWayRestriction && viaEdges != null)
+            throw new IllegalArgumentException("for node restrictions the viaEdges must be null");
+        if (isViaWayRestriction && viaEdges.isEmpty())
+            throw new IllegalArgumentException("for way restrictions there must at least one via edge");
+        if (isViaWayRestriction && viaNodes.size() != viaEdges.size() + 1)
+            throw new IllegalArgumentException("for way restrictions there must be one via node more than there are via edges");
+        this.isViaWayRestriction = isViaWayRestriction;
+        this.viaNodes = viaNodes;
+        this.fromEdges = fromEdges;
+        this.viaEdges = viaEdges;
+        this.toEdges = toEdges;
+    }
+
+    public boolean isViaWayRestriction() {
+        return isViaWayRestriction;
+    }
+
+    public IntArrayList getViaNodes() {
+        return viaNodes;
+    }
+
+    public IntArrayList getFromEdges() {
+        return fromEdges;
+    }
+
+    public IntArrayList getViaEdges() {
+        return viaEdges;
+    }
+
+    public IntArrayList getToEdges() {
+        return toEdges;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/OSMReader.java b/core/src/main/java/com/graphhopper/reader/osm/OSMReader.java
index 8f985e097..c7676adbc 100644
--- a/core/src/main/java/com/graphhopper/reader/osm/OSMReader.java
+++ b/core/src/main/java/com/graphhopper/reader/osm/OSMReader.java
@@ -17,10 +17,11 @@
  */
 package com.graphhopper.reader.osm;
 
-import com.carrotsearch.hppc.IntLongMap;
+import com.carrotsearch.hppc.IntIntMap;
 import com.carrotsearch.hppc.LongArrayList;
-import com.graphhopper.coll.GHIntLongHashMap;
-import com.graphhopper.coll.GHLongHashSet;
+import com.carrotsearch.hppc.LongHashSet;
+import com.carrotsearch.hppc.LongSet;
+import com.carrotsearch.hppc.cursors.LongCursor;
 import com.graphhopper.coll.GHLongLongHashMap;
 import com.graphhopper.reader.ReaderElement;
 import com.graphhopper.reader.ReaderNode;
@@ -37,7 +38,7 @@ import com.graphhopper.routing.util.EncodingManager;
 import com.graphhopper.routing.util.OSMParsers;
 import com.graphhopper.routing.util.countryrules.CountryRule;
 import com.graphhopper.routing.util.countryrules.CountryRuleFactory;
-import com.graphhopper.routing.util.parsers.TurnCostParser;
+import com.graphhopper.routing.util.parsers.RestrictionSetter;
 import com.graphhopper.search.EdgeKVStorage;
 import com.graphhopper.storage.BaseGraph;
 import com.graphhopper.storage.IntsRef;
@@ -54,6 +55,7 @@ import java.io.IOException;
 import java.util.*;
 import java.util.function.LongToIntFunction;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import static com.graphhopper.search.EdgeKVStorage.KeyValue.*;
 import static com.graphhopper.util.Helper.nf;
@@ -80,6 +82,7 @@ public class OSMReader {
     private final EncodingManager encodingManager;
     private final OSMParsers osmParsers;
     private final DistanceCalc distCalc = DistanceCalcEarth.DIST_EARTH;
+    private final RestrictionSetter restrictionSetter;
     private ElevationProvider eleProvider = ElevationProvider.NOOP;
     private AreaIndex<CustomArea> areaIndex;
     private CountryRuleFactory countryRuleFactory = null;
@@ -91,9 +94,8 @@ public class OSMReader {
     private long zeroCounter = 0;
 
     private GHLongLongHashMap osmWayIdToRelationFlagsMap = new GHLongLongHashMap(200, .5f);
-    // stores osm way ids used by relations to identify which edge ids needs to be mapped later
-    private GHLongHashSet osmWayIdSet = new GHLongHashSet();
-    private IntLongMap edgeIdToOsmWayIdMap;
+    private WayToEdgesMap restrictedWaysToEdgesMap = new WayToEdgesMap();
+    private List<ReaderRelation> restrictionRelations = new ArrayList<>();
 
     public OSMReader(BaseGraph baseGraph, EncodingManager encodingManager, OSMParsers osmParsers, OSMReaderConfig config) {
         this.baseGraph = baseGraph;
@@ -101,6 +103,7 @@ public class OSMReader {
         this.config = config;
         this.nodeAccess = baseGraph.getNodeAccess();
         this.osmParsers = osmParsers;
+        this.restrictionSetter = new RestrictionSetter(baseGraph);
 
         simplifyAlgo.setMaxDistance(config.getMaxWayPointDistance());
         simplifyAlgo.setElevationMaxDistance(config.getElevationMaxWayPointDistance());
@@ -172,9 +175,11 @@ public class OSMReader {
         osmDataDate = waySegmentParser.getTimeStamp();
         if (baseGraph.getNodes() == 0)
             throw new RuntimeException("Graph after reading OSM must not be empty");
+        releaseEverythingExceptRestrictionData();
+        addRestrictionsToGraph();
+        releaseRestrictionData();
         LOGGER.info("Finished reading OSM file: {}, nodes: {}, edges: {}, zero distance edges: {}",
                 osmFile.getAbsolutePath(), nf(baseGraph.getNodes()), nf(baseGraph.getEdges()), nf(zeroCounter));
-        finishedReading();
     }
 
     /**
@@ -361,9 +366,7 @@ public class OSMReader {
         }
 
         checkDistance(edge);
-        if (osmWayIdSet.contains(way.getId())) {
-            getEdgeIdToOsmWayIdMap().put(edge.getEdge(), way.getId());
-        }
+        restrictedWaysToEdgesMap.putIfReserved(way.getId(), edge.getEdge());
     }
 
     private void checkCoordinates(int nodeIndex, GHPoint point) {
@@ -521,59 +524,97 @@ public class OSMReader {
             }
         }
 
-        // we keep the osm way ids that occur in turn restriction relations, because this way we know for which GH edges
-        // we need to remember the associated osm way id. this is just an optimization that is supposed to save
-        // memory compared to simply storing the osm way ids in a long array where the array index is the GH edge
-        // id.
-        for (OSMTurnRestriction turnRestriction : OSMRestrictionRelationParser.createTurnRestrictions(relation, LOGGER::warn)) {
-            osmWayIdSet.add(turnRestriction.getOsmIdFrom());
-            osmWayIdSet.add(turnRestriction.getOsmIdTo());
-        }
+        Arrays.stream(RestrictionConverter.getRestrictedWayIds(relation))
+                .forEach(restrictedWaysToEdgesMap::reserve);
     }
 
     /**
      * This method is called for each relation during the second pass of {@link WaySegmentParser}
-     * We use it to set turn restrictions.
+     * We use it to save the relations and process them afterwards.
      */
     protected void processRelation(ReaderRelation relation, LongToIntFunction getIdForOSMNodeId) {
-        if (turnCostStorage != null) {
-            List<OSMTurnRestriction> turnRestrictions = OSMRestrictionRelationParser.createTurnRestrictions(relation, s -> {
-            });
-            if (turnRestrictions.isEmpty())
-                return;
-            TurnCostParser.ExternalInternalMap map = new TurnCostParser.ExternalInternalMap() {
-                @Override
-                public int getInternalNodeIdOfOsmNode(long nodeOsmId) {
-                    return getIdForOSMNodeId.applyAsInt(nodeOsmId);
-                }
+        if (turnCostStorage != null)
+            if (RestrictionConverter.isTurnRestriction(relation)) {
+                long osmViaNode = RestrictionConverter.getViaNodeIfViaNodeRestriction(relation);
+                if (osmViaNode >= 0) {
+                    int viaNode = getIdForOSMNodeId.applyAsInt(osmViaNode);
+                    // only include the restriction if the corresponding node wasn't excluded
+                    if (viaNode >= 0) {
+                        relation.setTag("graphhopper:via_node", viaNode);
+                        restrictionRelations.add(relation);
+                    }
+                } else
+                    // not a via-node restriction -> simply add it as is
+                    restrictionRelations.add(relation);
+            }
+    }
 
-                @Override
-                public long getOsmIdOfInternalEdge(int edgeId) {
-                    return getEdgeIdToOsmWayIdMap().get(edgeId);
+    private void addRestrictionsToGraph() {
+        // The OSM restriction format is explained here: https://wiki.openstreetmap.org/wiki/Relation:restriction
+        List<Triple<ReaderRelation, GraphRestriction, RestrictionMembers>> restrictions = new ArrayList<>(restrictionRelations.size());
+        for (ReaderRelation restrictionRelation : restrictionRelations) {
+            try {
+                // convert the OSM relation topology to the graph representation. this only needs to be done once for all
+                // vehicle types (we also want to print warnings only once)
+                restrictions.add(RestrictionConverter.convert(restrictionRelation, baseGraph, restrictedWaysToEdgesMap::getEdges));
+            } catch (OSMRestrictionException e) {
+                warnOfRestriction(restrictionRelation, e);
+            }
+        }
+        // The restriction type depends on the vehicle, or at least not all restrictions affect every vehicle type.
+        // We handle the restrictions for one vehicle after another.
+        for (RestrictionTagParser restrictionTagParser : osmParsers.getRestrictionTagParsers()) {
+            LongSet viaWaysUsedByOnlyRestrictions = new LongHashSet();
+            List<Pair<GraphRestriction, RestrictionType>> restrictionsWithType = new ArrayList<>(restrictions.size());
+            for (Triple<ReaderRelation, GraphRestriction, RestrictionMembers> r : restrictions) {
+                if (r.second == null)
+                    // this relation was found to be invalid by another restriction tag parser already
+                    continue;
+                try {
+                    RestrictionTagParser.Result res = restrictionTagParser.parseRestrictionTags(r.first.getTags());
+                    if (res == null)
+                        // this relation is ignored by the current restriction tag parser
+                        continue;
+                    RestrictionConverter.checkIfCompatibleWithRestriction(r.second, res.getRestriction());
+                    // we ignore 'only' via-way restrictions that share the same via way, because these would require adding
+                    // multiple artificial edges, see here: https://github.com/graphhopper/graphhopper/pull/2689#issuecomment-1306769694
+                    if (r.second.isViaWayRestriction() && res.getRestrictionType() == RestrictionType.ONLY)
+                        for (LongCursor viaWay : r.third.getViaWays())
+                            if (!viaWaysUsedByOnlyRestrictions.add(viaWay.value))
+                                throw new OSMRestrictionException("has a member with role 'via' that is also used as 'via' member by another 'only' restriction. GraphHopper cannot handle this.");
+                    restrictionsWithType.add(new Pair<>(r.second, res.getRestrictionType()));
+                } catch (OSMRestrictionException e) {
+                    warnOfRestriction(r.first, e);
+                    // we only want to print a warning once for each restriction relation, so we make sure this
+                    // restriction is ignored for the other vehicles
+                    r.second = null;
                 }
-            };
-            for (OSMTurnRestriction turnRestriction : turnRestrictions) {
-                int viaNode = map.getInternalNodeIdOfOsmNode(turnRestriction.getViaOsmNodeId());
-                // street with restriction was not included (access or tag limits etc)
-                if (viaNode >= 0)
-                    osmParsers.handleTurnRestrictionTags(turnRestriction, map, baseGraph);
             }
+            restrictionSetter.setRestrictions(restrictionsWithType, restrictionTagParser.getTurnCostEnc());
         }
     }
 
-    private IntLongMap getEdgeIdToOsmWayIdMap() {
-        // todo: is this lazy initialization really advantageous?
-        if (edgeIdToOsmWayIdMap == null)
-            edgeIdToOsmWayIdMap = new GHIntLongHashMap(osmWayIdSet.size(), 0.5f);
+    public IntIntMap getArtificialEdgesByEdges() {
+        return restrictionSetter.getArtificialEdgesByEdges();
+    }
 
-        return edgeIdToOsmWayIdMap;
+    private static void warnOfRestriction(ReaderRelation restrictionRelation, OSMRestrictionException e) {
+        // we do not log exceptions with an empty message
+        if (!e.isWithoutWarning()) {
+            restrictionRelation.getTags().remove("graphhopper:via_node");
+            List<String> members = restrictionRelation.getMembers().stream().map(m -> m.getRole() + " " + m.getType().toString().toLowerCase() + " " + m.getRef()).collect(Collectors.toList());
+            LOGGER.warn("Restriction relation " + restrictionRelation.getId() + " " + e.getMessage() + ". tags: " + restrictionRelation.getTags() + ", members: " + members + ". Relation ignored.");
+        }
     }
 
-    private void finishedReading() {
+    private void releaseEverythingExceptRestrictionData() {
         eleProvider.release();
         osmWayIdToRelationFlagsMap = null;
-        osmWayIdSet = null;
-        edgeIdToOsmWayIdMap = null;
+    }
+
+    private void releaseRestrictionData() {
+        restrictedWaysToEdgesMap = null;
+        restrictionRelations = null;
     }
 
     IntsRef getRelFlagsMap(long osmId) {
diff --git a/core/src/main/java/com/graphhopper/reader/osm/OSMRestrictionException.java b/core/src/main/java/com/graphhopper/reader/osm/OSMRestrictionException.java
new file mode 100644
index 000000000..dc0940e01
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/reader/osm/OSMRestrictionException.java
@@ -0,0 +1,34 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+public class OSMRestrictionException extends Exception {
+
+    public static OSMRestrictionException withoutWarning() {
+        return new OSMRestrictionException("");
+    }
+
+    public OSMRestrictionException(String message) {
+        super(message);
+    }
+
+    public boolean isWithoutWarning() {
+        return getMessage().isEmpty();
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/OSMRestrictionRelationParser.java b/core/src/main/java/com/graphhopper/reader/osm/OSMRestrictionRelationParser.java
deleted file mode 100644
index dd28b8d5f..000000000
--- a/core/src/main/java/com/graphhopper/reader/osm/OSMRestrictionRelationParser.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- *  Licensed to GraphHopper GmbH under one or more contributor
- *  license agreements. See the NOTICE file distributed with this work for
- *  additional information regarding copyright ownership.
- *
- *  GraphHopper GmbH licenses this file to you under the Apache License,
- *  Version 2.0 (the "License"); you may not use this file except in
- *  compliance with the License. You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.graphhopper.reader.osm;
-
-import com.carrotsearch.hppc.LongArrayList;
-import com.carrotsearch.hppc.cursors.LongCursor;
-import com.graphhopper.reader.ReaderElement;
-import com.graphhopper.reader.ReaderRelation;
-
-import java.util.*;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-import static java.util.Collections.emptyList;
-
-public class OSMRestrictionRelationParser {
-    static List<OSMTurnRestriction> createTurnRestrictions(ReaderRelation relation, Consumer<String> warningConsumer) {
-        // see here: https://wiki.openstreetmap.org/wiki/Relation:restriction
-        if (!"restriction".equals(relation.getTag("type")))
-            return emptyList();
-
-        String restriction = relation.getTag("restriction");
-        List<String> limitedRestrictions = relation.getTags().entrySet().stream()
-                .filter(t -> t.getKey().startsWith("restriction:"))
-                // restriction:bicycle=give_way seems quite common in France, but since it isn't a 'strict' turn
-                // restriction we ignore it here. We also want to prevent warnings about combined
-                // restriction+restriction: tags in this case (see below).
-                .filter(e -> !"give_way".equals(e.getValue()))
-                .map(Map.Entry::getKey)
-                .collect(Collectors.toList());
-        List<String> exceptVehicles = relation.hasTag("except")
-                // todo: there are also some occurrences of except=resident(s), destination or delivery
-                ? Arrays.stream(relation.getTag("except").split(";")).map(String::trim).collect(Collectors.toList())
-                : emptyList();
-        if (restriction != null) {
-            // the 'restriction' tag limits the turns for all vehicles, unless this is modified by the 'except' tag
-            if (!limitedRestrictions.isEmpty()) {
-                warningConsumer.accept("Restriction relation " + relation.getId() + " has a 'restriction' tag, but also 'restriction:' tags: " + relation.getTags() + ". Relation ignored.");
-                return emptyList();
-            }
-            return createTurnRestrictions(relation, restriction, warningConsumer).stream()
-                    .peek(r -> r.setVehicleTypesExcept(exceptVehicles))
-                    .collect(Collectors.toList());
-        } else {
-            // if there is no 'restriction' tag there still might be 'restriction:xyz' tags that only affect certain vehicles
-            if (limitedRestrictions.isEmpty()) {
-                warningConsumer.accept("Restriction relation " + relation.getId() + " neither has a 'restriction' nor 'restriction:' tags: " + relation.getTags() + ". Relation ignored.");
-                return emptyList();
-            }
-            if (!exceptVehicles.isEmpty() && limitedRestrictions.stream().noneMatch(r -> r.startsWith("restriction:conditional"))) {
-                warningConsumer.accept("Restriction relation " + relation.getId() + " has an 'except', but no 'restriction' or 'restriction:conditional' tag: " + relation.getTags() + ". Relation ignored.");
-                return emptyList();
-            }
-            return limitedRestrictions.stream()
-                    // We do not consider the restriction[:<transportation_mode>]:conditional tag so far
-                    .filter(r -> !r.contains("conditional"))
-                    .flatMap(r -> {
-                        String vehicle = r.replace("restriction:", "").trim();
-                        return createTurnRestrictions(relation, relation.getTag(r), warningConsumer).stream()
-                                .peek(rb -> rb.setVehicleTypeRestricted(vehicle));
-                    })
-                    .collect(Collectors.toList());
-        }
-    }
-
-    private static List<OSMTurnRestriction> createTurnRestrictions(ReaderRelation relation, String restriction, Consumer<String> warningConsumer) {
-        OSMTurnRestriction.RestrictionType type = parseRestrictionValue(restriction);
-
-        if (type == OSMTurnRestriction.RestrictionType.UNSUPPORTED)
-            return Collections.emptyList();
-
-        LongArrayList fromWays = new LongArrayList(1);
-        LongArrayList toWays = new LongArrayList(1);
-        // we use -1 to indicate 'missing', which is fine because we exclude negative OSM IDs (see #2652)
-        long viaNodeID = -1;
-        for (ReaderRelation.Member member : relation.getMembers()) {
-            if ("from".equals(member.getRole())) {
-                if (member.getType() != ReaderElement.Type.WAY) {
-                    warningConsumer.accept("Restriction relation " + relation.getId() + " has a member with role 'from'" +
-                            " and type '" + member.getType() + "', but it should be of type 'way'" + relation.getTags() + ". Relation ignored.");
-                    return emptyList();
-                }
-                if (!fromWays.isEmpty() && !"no_entry".equals(restriction)) {
-                    warningConsumer.accept("Restriction relation " + relation.getId() + " has multiple members with role" +
-                            " 'from' even though it is not a 'no_entry' restriction: " + relation.getTags() + ". Relation ignored.");
-                    return emptyList();
-                }
-                fromWays.add(member.getRef());
-            } else if ("to".equals(member.getRole())) {
-                if (member.getType() != ReaderElement.Type.WAY) {
-                    warningConsumer.accept("Restriction relation " + relation.getId() + " has a member with role 'to' and" +
-                            " type '" + member.getType() + "', but it should be of type 'way': " + relation.getTags() + ". Relation ignored.");
-                    return emptyList();
-                }
-                if (!toWays.isEmpty() && !"no_exit".equals(restriction)) {
-                    warningConsumer.accept("Restriction relation " + relation.getId() + " has multiple members with role " +
-                            "'to' even though it is not a 'no_exit' restriction: " + relation.getTags() + ". Relation ignored.");
-                    return emptyList();
-                }
-                toWays.add(member.getRef());
-            } else if ("via".equals(member.getRole())) {
-                if (viaNodeID >= 0)
-                    // so far we only support restriction relations with a single via member
-                    return emptyList();
-                if (member.getType() != ReaderElement.Type.NODE) {
-                    // so far we only support via members that are nodes
-                    return emptyList();
-                }
-                viaNodeID = member.getRef();
-            } else if ("location_hint".equals(member.getRole())) {
-                // location_hint is deprecated and should no longer be used according to the wiki, but we do not warn
-                // about it, or even ignore the relation in this case, because maybe not everyone is happy to remove it.
-            } else {
-                warningConsumer.accept("Restriction relation " + relation.getId() + " has a member with an unknown role '"
-                        + member.getRole() + "': " + relation.getTags() + ". Relation ignored.");
-                return emptyList();
-            }
-        }
-
-        if (viaNodeID < 0) {
-            warningConsumer.accept("Restriction relation " + relation.getId() + " has no member with role 'via': " + relation.getTags() + ". Relation ignored");
-            return emptyList();
-        }
-        if (fromWays.size() < 1 || toWays.size() < 1) {
-            warningConsumer.accept("Restriction relation " + relation.getId() + " is missing a member with role 'from' or 'to': " + relation.getTags() + ". Relation ignored");
-            return emptyList();
-        }
-        List<OSMTurnRestriction> res = new ArrayList<>(fromWays.size() * toWays.size());
-        for (LongCursor from : fromWays)
-            for (LongCursor to : toWays)
-                res.add(new OSMTurnRestriction(from.value, viaNodeID, to.value, type));
-        return res;
-    }
-
-    private static OSMTurnRestriction.RestrictionType parseRestrictionValue(String restrictionType) {
-        switch (restrictionType) {
-            case "no_left_turn":
-            case "no_right_turn":
-            case "no_straight_on":
-            case "no_u_turn":
-            case "no_entry":
-            case "no_exit":
-                return OSMTurnRestriction.RestrictionType.NOT;
-            case "only_left_turn":
-            case "only_right_turn":
-            case "only_straight_on":
-            case "only_u_turn":
-                return OSMTurnRestriction.RestrictionType.ONLY;
-            default:
-                return OSMTurnRestriction.RestrictionType.UNSUPPORTED;
-        }
-    }
-}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/OSMTurnRestriction.java b/core/src/main/java/com/graphhopper/reader/osm/OSMTurnRestriction.java
deleted file mode 100644
index 7e4d3cbab..000000000
--- a/core/src/main/java/com/graphhopper/reader/osm/OSMTurnRestriction.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- *  Licensed to GraphHopper GmbH under one or more contributor
- *  license agreements. See the NOTICE file distributed with this work for
- *  additional information regarding copyright ownership.
- *
- *  GraphHopper GmbH licenses this file to you under the Apache License,
- *  Version 2.0 (the "License"); you may not use this file except in
- *  compliance with the License. You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package com.graphhopper.reader.osm;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Helper object which gives node cost entries for a given OSM-relation of type "restriction"
- *
- * @author Karl Hübner
- */
-public class OSMTurnRestriction {
-    private final long fromOsmWayId;
-    private final long viaOsmNodeId;
-    private final long toOsmWayId;
-    private final RestrictionType restriction;
-    // vehicleTypeRestricted contains the dedicated vehicle type
-    // example: restriction:bus = no_left_turn => vehicleTypeRestricted = "bus";
-    private String vehicleTypeRestricted;
-    private List<String> vehicleTypesExcept;
-
-    public OSMTurnRestriction(long fromWayID, long viaNodeID, long toWayID, RestrictionType restrictionType) {
-        this.fromOsmWayId = fromWayID;
-        this.viaOsmNodeId = viaNodeID;
-        this.toOsmWayId = toWayID;
-        this.restriction = restrictionType;
-        this.vehicleTypeRestricted = "";
-        this.vehicleTypesExcept = new ArrayList<>();
-    }
-
-    public long getOsmIdFrom() {
-        return fromOsmWayId;
-    }
-
-    public long getOsmIdTo() {
-        return toOsmWayId;
-    }
-
-    public long getViaOsmNodeId() {
-        return viaOsmNodeId;
-    }
-
-    public RestrictionType getRestriction() {
-        return restriction;
-    }
-
-    public String getVehicleTypeRestricted() {
-        return vehicleTypeRestricted;
-    }
-
-    public void setVehicleTypeRestricted(String vehicleTypeRestricted) {
-        this.vehicleTypeRestricted = vehicleTypeRestricted;
-    }
-
-    public List<String> getVehicleTypesExcept() {
-        return vehicleTypesExcept;
-    }
-
-    public void setVehicleTypesExcept(List<String> vehicleTypesExcept) {
-        this.vehicleTypesExcept = vehicleTypesExcept;
-    }
-
-    /**
-     * For a conditional turn restriction, test each vehicle type to verify if it is concerned.
-     * For a normal turn restriction (non conditional), the restriction is necessary considered.
-     */
-    public boolean isVehicleTypeConcernedByTurnRestriction(Collection<String> vehicleTypes) {
-        // if the restriction explicitly does not apply for one of the vehicles we do not accept it
-        if (!Collections.disjoint(vehicleTypes, vehicleTypesExcept)) {
-            return false;
-        }
-        return vehicleTypeRestricted.isEmpty() || vehicleTypes.contains(vehicleTypeRestricted);
-    }
-
-    @Override
-    public String toString() {
-        return "*-(" + fromOsmWayId + ")->" + viaOsmNodeId + "-(" + toOsmWayId + ")->*";
-    }
-
-    public enum RestrictionType {
-        UNSUPPORTED, NOT, ONLY
-    }
-
-}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/Pair.java b/core/src/main/java/com/graphhopper/reader/osm/Pair.java
new file mode 100644
index 000000000..6ad2aeeba
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/reader/osm/Pair.java
@@ -0,0 +1,29 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+public class Pair<F, S> {
+    public F first;
+    public S second;
+
+    public Pair(F first, S second) {
+        this.first = first;
+        this.second = second;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/RestrictionConverter.java b/core/src/main/java/com/graphhopper/reader/osm/RestrictionConverter.java
new file mode 100644
index 000000000..67f2451e7
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/reader/osm/RestrictionConverter.java
@@ -0,0 +1,182 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+import com.carrotsearch.hppc.LongArrayList;
+import com.carrotsearch.hppc.cursors.IntCursor;
+import com.carrotsearch.hppc.cursors.LongCursor;
+import com.graphhopper.reader.ReaderElement;
+import com.graphhopper.reader.ReaderRelation;
+import com.graphhopper.storage.BaseGraph;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.function.LongFunction;
+
+public class RestrictionConverter {
+    private static final Logger LOGGER = LoggerFactory.getLogger(RestrictionConverter.class);
+    private static final long[] EMPTY_LONG_ARRAY_LIST = new long[0];
+
+    public static boolean isTurnRestriction(ReaderRelation relation) {
+        return "restriction".equals(relation.getTag("type"));
+    }
+
+    public static long[] getRestrictedWayIds(ReaderRelation relation) {
+        if (!isTurnRestriction(relation))
+            return EMPTY_LONG_ARRAY_LIST;
+        return relation.getMembers().stream()
+                .filter(m -> m.getType() == ReaderElement.Type.WAY)
+                .filter(m -> "from".equals(m.getRole()) || "via".equals(m.getRole()) || "to".equals(m.getRole()))
+                .mapToLong(ReaderRelation.Member::getRef)
+                .toArray();
+    }
+
+    public static long getViaNodeIfViaNodeRestriction(ReaderRelation relation) {
+        return relation.getMembers().stream()
+                .filter(m -> m.getType().equals(ReaderElement.Type.NODE))
+                .filter(m -> "via".equals(m.getRole()))
+                .mapToLong(ReaderRelation.Member::getRef)
+                .findFirst()
+                .orElse(-1);
+    }
+
+    /**
+     * OSM restriction relations specify turn restrictions between OSM ways (of course). This method converts such a
+     * relation into a 'graph' representation, where the turn restrictions are specified in terms of edge/node IDs instead
+     * of OSM IDs.
+     *
+     * @throws OSMRestrictionException if the given relation is either not valid in some way and/or cannot be handled and
+     *                                 shall be ignored
+     */
+    public static Triple<ReaderRelation, GraphRestriction, RestrictionMembers> convert(ReaderRelation relation, BaseGraph baseGraph, LongFunction<Iterator<IntCursor>> edgesByWay) throws OSMRestrictionException {
+        if (!isTurnRestriction(relation))
+            throw new IllegalArgumentException("expected a turn restriction: " + relation.getTags());
+        RestrictionMembers restrictionMembers = extractMembers(relation);
+        if (!membersExist(restrictionMembers, edgesByWay, relation))
+            throw OSMRestrictionException.withoutWarning();
+        // every OSM way might be split into *multiple* edges, so now we need to figure out which edges are the ones
+        // that are actually part of the given relation
+        WayToEdgeConverter wayToEdgeConverter = new WayToEdgeConverter(baseGraph, edgesByWay);
+        if (restrictionMembers.isViaWay()) {
+            WayToEdgeConverter.EdgeResult res = wayToEdgeConverter
+                    .convertForViaWays(restrictionMembers.getFromWays(), restrictionMembers.getViaWays(), restrictionMembers.getToWays());
+            return new Triple<>(relation, GraphRestriction.way(res.getFromEdges(), res.getViaEdges(), res.getToEdges(), res.getNodes()), restrictionMembers);
+        } else {
+            int viaNode = relation.getTag("graphhopper:via_node", -1);
+            if (viaNode < 0)
+                throw new IllegalStateException("For some reason we did not set graphhopper:via_node for this relation: " + relation.getId());
+            WayToEdgeConverter.NodeResult res = wayToEdgeConverter
+                    .convertForViaNode(restrictionMembers.getFromWays(), viaNode, restrictionMembers.getToWays());
+            return new Triple<>(relation, GraphRestriction.node(res.getFromEdges(), viaNode, res.getToEdges()), restrictionMembers);
+        }
+    }
+
+    private static boolean membersExist(RestrictionMembers members, LongFunction<Iterator<IntCursor>> edgesByWay, ReaderRelation relation) {
+        for (LongCursor c : members.getAllWays())
+            if (!edgesByWay.apply(c.value).hasNext()) {
+                // this happens for example at the map borders or when certain ways like footways are excluded
+                LOGGER.debug("Restriction relation " + relation.getId() + " uses excluded way " + c.value + ". Relation ignored.");
+                return false;
+            }
+        return true;
+    }
+
+    public static void checkIfCompatibleWithRestriction(GraphRestriction g, String restriction) throws OSMRestrictionException {
+        if (g.getFromEdges().size() > 1 && !"no_entry".equals(restriction))
+            throw new OSMRestrictionException("has multiple members with role 'from' even though it is not a 'no_entry' restriction");
+        if (g.getToEdges().size() > 1 && !"no_exit".equals(restriction))
+            throw new OSMRestrictionException("has multiple members with role 'to' even though it is not a 'no_exit' restriction");
+    }
+
+    public static RestrictionMembers extractMembers(ReaderRelation relation) throws OSMRestrictionException {
+        // we use -1 to indicate 'missing', which is fine because we exclude negative OSM IDs (see #2652)
+        long viaOSMNode = -1;
+        LongArrayList fromWays = new LongArrayList();
+        LongArrayList viaWays = new LongArrayList();
+        LongArrayList toWays = new LongArrayList();
+        for (ReaderRelation.Member member : relation.getMembers()) {
+            if ("from".equals(member.getRole())) {
+                if (member.getType() != ReaderElement.Type.WAY)
+                    throw new OSMRestrictionException("has a member with role 'from' and type '" + member.getType() + "', but it should be of type 'way'");
+                fromWays.add(member.getRef());
+            } else if ("to".equals(member.getRole())) {
+                if (member.getType() != ReaderElement.Type.WAY)
+                    throw new OSMRestrictionException("has a member with role 'to' and type '" + member.getType() + "', but it should be of type 'way'");
+                toWays.add(member.getRef());
+            } else if ("via".equals(member.getRole())) {
+                if (member.getType() == ReaderElement.Type.NODE) {
+                    if (viaOSMNode >= 0)
+                        throw new OSMRestrictionException("has multiple members with role 'via' and type 'node', but multiple via-members are only allowed when they are of type: 'way'");
+                    // note that we check for combined usage of via nodes and ways later on
+                    viaOSMNode = member.getRef();
+                } else if (member.getType() == ReaderElement.Type.WAY) {
+                    // note that we check for combined usage of via nodes and ways later on
+                    viaWays.add(member.getRef());
+                } else
+                    throw new OSMRestrictionException("has a member with role 'via' and" +
+                            " type '" + member.getType() + "', but it should be of type 'node' or 'way'");
+            } else if ("location_hint".equals(member.getRole())) {
+                // location_hint is deprecated and should no longer be used according to the wiki, but we do not warn
+                // about it, or even ignore the relation in this case, because maybe not everyone is happy to remove it.
+            } else if (member.getRole().trim().isEmpty())
+                throw new OSMRestrictionException("has a member with an empty role");
+            else
+                throw new OSMRestrictionException("has a member with an unknown role '" + member.getRole() + "'");
+        }
+        if (fromWays.isEmpty() && toWays.isEmpty())
+            throw new OSMRestrictionException("has no member with role 'from' and 'to'");
+        else if (fromWays.isEmpty())
+            throw new OSMRestrictionException("has no member with role 'from'");
+        else if (toWays.isEmpty())
+            throw new OSMRestrictionException("has no member with role 'to'");
+
+        if (fromWays.size() > 1 && toWays.size() > 1)
+            throw new OSMRestrictionException("has multiple members with role 'from' and 'to'");
+        checkTags(fromWays, toWays, relation.getTags());
+        if (viaOSMNode >= 0 && !viaWays.isEmpty())
+            throw new OSMRestrictionException("has members with role 'via' of type 'node' and 'way', but only one type is allowed");
+        else if (viaOSMNode >= 0)
+            return RestrictionMembers.viaNode(viaOSMNode, fromWays, toWays);
+        else if (!viaWays.isEmpty())
+            return RestrictionMembers.viaWay(fromWays, viaWays, toWays);
+        else
+            throw new OSMRestrictionException("has no member with role 'via'");
+    }
+
+    private static void checkTags(LongArrayList fromWays, LongArrayList toWays, Map<String, Object> tags) throws OSMRestrictionException {
+        // the exact restriction value depends on the vehicle type, but we can already print a warning for certain
+        // cases here, so later we do not print such warnings for every single vehicle.
+        boolean hasNoEntry = false;
+        boolean hasNoExit = false;
+        for (Map.Entry<String, Object> e : tags.entrySet()) {
+            if (e.getKey().startsWith("restriction")) {
+                if (e.getValue() != null && ((String) e.getValue()).startsWith("no_entry"))
+                    hasNoEntry = true;
+                if (e.getValue() != null && ((String) e.getValue()).startsWith("no_exit"))
+                    hasNoExit = true;
+            }
+        }
+        if (fromWays.size() > 1 && !hasNoEntry)
+            throw new OSMRestrictionException("has multiple members with role 'from' even though it is not a 'no_entry' restriction");
+        if (toWays.size() > 1 && !hasNoExit)
+            throw new OSMRestrictionException("has multiple members with role 'to' even though it is not a 'no_exit' restriction");
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/RestrictionMembers.java b/core/src/main/java/com/graphhopper/reader/osm/RestrictionMembers.java
new file mode 100644
index 000000000..d44f9bee0
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/reader/osm/RestrictionMembers.java
@@ -0,0 +1,73 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+import com.carrotsearch.hppc.LongArrayList;
+
+public class RestrictionMembers {
+    private final boolean isViaWay;
+    private final long viaOSMNode;
+    private final LongArrayList fromWays;
+    private final LongArrayList viaWays;
+    private final LongArrayList toWays;
+
+    public static RestrictionMembers viaNode(long viaOSMNode, LongArrayList fromWays, LongArrayList toWays) {
+        return new RestrictionMembers(false, viaOSMNode, fromWays, null, toWays);
+    }
+
+    public static RestrictionMembers viaWay(LongArrayList fromWays, LongArrayList viaWays, LongArrayList toWays) {
+        return new RestrictionMembers(true, -1, fromWays, viaWays, toWays);
+    }
+
+    private RestrictionMembers(boolean isViaWay, long viaOSMNode, LongArrayList fromWays, LongArrayList viaWays, LongArrayList toWays) {
+        this.isViaWay = isViaWay;
+        this.viaOSMNode = viaOSMNode;
+        this.fromWays = fromWays;
+        this.viaWays = viaWays;
+        this.toWays = toWays;
+    }
+
+    public boolean isViaWay() {
+        return isViaWay;
+    }
+
+    public long getViaOSMNode() {
+        return viaOSMNode;
+    }
+
+    public LongArrayList getFromWays() {
+        return fromWays;
+    }
+
+    public LongArrayList getViaWays() {
+        return viaWays;
+    }
+
+    public LongArrayList getToWays() {
+        return toWays;
+    }
+
+    public LongArrayList getAllWays() {
+        LongArrayList result = new LongArrayList(fromWays.size() + toWays.size() + (isViaWay ? viaWays.size() : 0));
+        result.addAll(fromWays);
+        if (isViaWay) result.addAll(viaWays);
+        result.addAll(toWays);
+        return result;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/RestrictionTagParser.java b/core/src/main/java/com/graphhopper/reader/osm/RestrictionTagParser.java
new file mode 100644
index 000000000..e6ba31c02
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/reader/osm/RestrictionTagParser.java
@@ -0,0 +1,134 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+import com.graphhopper.routing.ev.DecimalEncodedValue;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static java.util.Collections.emptyList;
+
+/**
+ * Parses the OSM restriction tags for given vehicle types / transportation modes.
+ */
+public class RestrictionTagParser {
+    private final List<String> vehicleTypes;
+    private final DecimalEncodedValue turnCostEnc;
+
+    public RestrictionTagParser(List<String> vehicleTypes, DecimalEncodedValue turnCostEnc) {
+        this.vehicleTypes = vehicleTypes;
+        this.turnCostEnc = turnCostEnc;
+    }
+
+    public DecimalEncodedValue getTurnCostEnc() {
+        return turnCostEnc;
+    }
+
+    public Result parseRestrictionTags(Map<String, Object> tags) throws OSMRestrictionException {
+        String restriction = (String) tags.get("restriction");
+        List<String> limitedRestrictions = tags.entrySet().stream()
+                .filter(t -> t.getKey().startsWith("restriction:"))
+                // restriction:bicycle=give_way seems quite common in France, but since it isn't a 'strict' turn
+                // restriction we ignore it here.
+                .filter(e -> !"give_way".equals(e.getValue()))
+                .map(Map.Entry::getKey)
+                .collect(Collectors.toList());
+        boolean hasGiveWay = tags.values().stream().anyMatch("give_way"::equals);
+        List<String> exceptVehicles = tags.containsKey("except")
+                // todo: there are also some occurrences of except=resident(s), destination or delivery
+                ? Arrays.stream(((String) tags.get("except")).split(";")).map(String::trim).collect(Collectors.toList())
+                : emptyList();
+        if (restriction != null) {
+            // the 'restriction' tag limits the turns for all vehicleTypes, unless this is modified by the 'except' tag
+            if (!limitedRestrictions.isEmpty())
+                // note that there is no warning if there is a restriction tag and restriction:*=give_way
+                throw new OSMRestrictionException("has a 'restriction' tag, but also 'restriction:' tags");
+            if (!Collections.disjoint(vehicleTypes, exceptVehicles))
+                return null;
+            return buildResult(restriction);
+        } else {
+            // if there is no 'restriction' tag there still might be 'restriction:xyz' tags that only affect certain vehicleTypes
+            if (limitedRestrictions.isEmpty())
+                if (!hasGiveWay)
+                    throw new OSMRestrictionException("neither has a 'restriction' nor 'restriction:' tags");
+                else
+                    // ignore, but no warning if there is only restriction:*=give_way
+                    throw OSMRestrictionException.withoutWarning();
+            if (!exceptVehicles.isEmpty() && limitedRestrictions.stream().noneMatch(r -> r.startsWith("restriction:conditional")))
+                throw new OSMRestrictionException("has an 'except', but no 'restriction' or 'restriction:conditional' tag");
+            Set<String> restrictions = limitedRestrictions.stream()
+                    // We do not consider the restriction[:<transportation_mode>]:conditional tag so far
+                    .filter(r -> !r.contains("conditional"))
+                    .filter(r -> vehicleTypes.contains(r.replace("restriction:", "").trim()))
+                    .map(r -> (String) tags.get(r))
+                    .collect(Collectors.toSet());
+            if (restrictions.size() > 1)
+                throw new OSMRestrictionException("contains multiple different restriction values: '" + restrictions + "'");
+            else if (restrictions.isEmpty())
+                return null;
+            else
+                return buildResult(restrictions.iterator().next());
+        }
+    }
+
+    private static Result buildResult(String restriction) throws OSMRestrictionException {
+        return new Result(parseRestrictionValue(restriction), restriction);
+    }
+
+    private static RestrictionType parseRestrictionValue(String restriction) throws OSMRestrictionException {
+        switch (restriction) {
+            case "no_left_turn":
+            case "no_right_turn":
+            case "no_straight_on":
+            case "no_u_turn":
+            case "no_entry":
+            case "no_exit":
+                return RestrictionType.NO;
+            case "only_left_turn":
+            case "only_right_turn":
+            case "only_straight_on":
+            case "only_u_turn":
+                return RestrictionType.ONLY;
+            case "no_right_turn_on_red":
+            case "no_left_turn_on_red":
+                throw OSMRestrictionException.withoutWarning();
+            default:
+                throw new OSMRestrictionException("uses unknown restriction value: '" + restriction + "'");
+        }
+    }
+
+    public static class Result {
+        private final RestrictionType restrictionType;
+        private final String restriction;
+
+        public Result(RestrictionType restrictionType, String restriction) {
+            this.restrictionType = restrictionType;
+            this.restriction = restriction;
+        }
+
+        public RestrictionType getRestrictionType() {
+            return restrictionType;
+        }
+
+        public String getRestriction() {
+            return restriction;
+        }
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/RestrictionType.java b/core/src/main/java/com/graphhopper/reader/osm/RestrictionType.java
new file mode 100644
index 000000000..9ed83aac8
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/reader/osm/RestrictionType.java
@@ -0,0 +1,23 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+public enum RestrictionType {
+    NO, ONLY
+}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/Triple.java b/core/src/main/java/com/graphhopper/reader/osm/Triple.java
new file mode 100644
index 000000000..5a88e22dd
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/reader/osm/Triple.java
@@ -0,0 +1,31 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+public class Triple<F, S, T> {
+    public F first;
+    public S second;
+    public T third;
+
+    public Triple(F first, S second, T third) {
+        this.first = first;
+        this.second = second;
+        this.third = third;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/WayToEdgeConverter.java b/core/src/main/java/com/graphhopper/reader/osm/WayToEdgeConverter.java
new file mode 100644
index 000000000..e12d1adb9
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/reader/osm/WayToEdgeConverter.java
@@ -0,0 +1,229 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+import com.carrotsearch.hppc.IntArrayList;
+import com.carrotsearch.hppc.LongArrayList;
+import com.carrotsearch.hppc.cursors.IntCursor;
+import com.carrotsearch.hppc.cursors.LongCursor;
+import com.graphhopper.storage.BaseGraph;
+import com.graphhopper.util.EdgeIteratorState;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.LongFunction;
+
+public class WayToEdgeConverter {
+    private final BaseGraph baseGraph;
+    private final LongFunction<Iterator<IntCursor>> edgesByWay;
+
+    public WayToEdgeConverter(BaseGraph baseGraph, LongFunction<Iterator<IntCursor>> edgesByWay) {
+        this.baseGraph = baseGraph;
+        this.edgesByWay = edgesByWay;
+    }
+
+    /**
+     * Finds the edge IDs associated with the given OSM ways that are adjacent to the given via-node.
+     * For each way there can be multiple edge IDs and there should be exactly one that is adjacent to the via-node
+     * for each way. Otherwise we throw {@link OSMRestrictionException}
+     */
+    public NodeResult convertForViaNode(LongArrayList fromWays, int viaNode, LongArrayList toWays) throws OSMRestrictionException {
+        if (fromWays.isEmpty() || toWays.isEmpty())
+            throw new IllegalArgumentException("There must be at least one from- and to-way");
+        if (fromWays.size() > 1 && toWays.size() > 1)
+            throw new IllegalArgumentException("There can only be multiple from- or to-ways, but not both");
+        NodeResult result = new NodeResult(fromWays.size(), toWays.size());
+        for (LongCursor fromWay : fromWays)
+            edgesByWay.apply(fromWay.value).forEachRemaining(e -> {
+                if (baseGraph.isAdjacentToNode(e.value, viaNode))
+                    result.fromEdges.add(e.value);
+            });
+        if (result.fromEdges.size() < fromWays.size())
+            throw new OSMRestrictionException("has from member ways that aren't adjacent to the via member node");
+        else if (result.fromEdges.size() > fromWays.size())
+            throw new OSMRestrictionException("has from member ways that aren't split at the via member node");
+
+        for (LongCursor toWay : toWays)
+            edgesByWay.apply(toWay.value).forEachRemaining(e -> {
+                if (baseGraph.isAdjacentToNode(e.value, viaNode))
+                    result.toEdges.add(e.value);
+            });
+        if (result.toEdges.size() < toWays.size())
+            throw new OSMRestrictionException("has to member ways that aren't adjacent to the via member node");
+        else if (result.toEdges.size() > toWays.size())
+            throw new OSMRestrictionException("has to member ways that aren't split at the via member node");
+        return result;
+    }
+
+    public static class NodeResult {
+        private final IntArrayList fromEdges;
+        private final IntArrayList toEdges;
+
+        public NodeResult(int numFrom, int numTo) {
+            fromEdges = new IntArrayList(numFrom);
+            toEdges = new IntArrayList(numTo);
+        }
+
+        public IntArrayList getFromEdges() {
+            return fromEdges;
+        }
+
+        public IntArrayList getToEdges() {
+            return toEdges;
+        }
+    }
+
+    /**
+     * Finds the edge IDs associated with the given OSM ways that are adjacent to each other. For example for given
+     * from-, via- and to-ways there can be multiple edges associated with each (because each way can be split into
+     * multiple edges). We then need to find the from-edge that is connected with one of the via-edges which in turn
+     * must be connected with one of the to-edges. We use DFS/backtracking to do this.
+     * There can also be *multiple* via-ways, but the concept is the same.
+     * Note that there can also be multiple from- or to-*ways*, but only one of each of them should be considered at a
+     * time. In contrast to the via-ways there are only multiple from/to-ways, because of restrictions like no_entry or
+     * no_exit where there can be multiple from- or to-members. So we need to find one edge-chain for each pair of from-
+     * and to-ways.
+     * Besides the edge IDs we also return the node IDs that connect the edges, so we can add turn restrictions at these
+     * nodes later.
+     */
+    public EdgeResult convertForViaWays(LongArrayList fromWays, LongArrayList viaWays, LongArrayList toWays) throws OSMRestrictionException {
+        if (fromWays.isEmpty() || toWays.isEmpty() || viaWays.isEmpty())
+            throw new IllegalArgumentException("There must be at least one from-, via- and to-way");
+        if (fromWays.size() > 1 && toWays.size() > 1)
+            throw new IllegalArgumentException("There can only be multiple from- or to-ways, but not both");
+        List<IntArrayList> solutions = new ArrayList<>();
+        for (LongCursor fromWay : fromWays)
+            for (LongCursor toWay : toWays)
+                findEdgeChain(fromWay.value, viaWays, toWay.value, solutions);
+        if (solutions.size() < fromWays.size() * toWays.size())
+            throw new OSMRestrictionException("has from/to member ways that aren't connected with the via member way(s)");
+        else if (solutions.size() > fromWays.size() * toWays.size())
+            throw new OSMRestrictionException("has from/to member ways that aren't split at the via member way(s)");
+        return buildResult(solutions, new EdgeResult(fromWays.size(), viaWays.size(), toWays.size()));
+    }
+
+    private static EdgeResult buildResult(List<IntArrayList> edgeChains, EdgeResult result) {
+        for (IntArrayList edgeChain : edgeChains) {
+            result.fromEdges.add(edgeChain.get(0));
+            if (result.nodes.isEmpty()) {
+                // the via-edges and nodes are the same for edge chain
+                for (int i = 1; i < edgeChain.size() - 3; i += 2) {
+                    result.nodes.add(edgeChain.get(i));
+                    result.viaEdges.add(edgeChain.get(i + 1));
+                }
+                result.nodes.add(edgeChain.get(edgeChain.size() - 2));
+            }
+            result.toEdges.add(edgeChain.get(edgeChain.size() - 1));
+        }
+        return result;
+    }
+
+    private void findEdgeChain(long fromWay, LongArrayList viaWays, long toWay, List<IntArrayList> solutions) throws OSMRestrictionException {
+        // For each edge chain there must be one edge associated with the from-way, one for each via-way and one
+        // associated with the to-way. We use DFS with backtracking to find all edge chains that connect an edge
+        // associated with the from-way with one associated with the to-way.
+        IntArrayList viaEdgesForViaWays = new IntArrayList(viaWays.size());
+        for (LongCursor c : viaWays) {
+            Iterator<IntCursor> iterator = edgesByWay.apply(c.value);
+            viaEdgesForViaWays.add(iterator.next().value);
+            if (iterator.hasNext())
+                throw new OSMRestrictionException("has via member way that isn't split at adjacent ways: " + c.value);
+        }
+        IntArrayList toEdges = listFromIterator(edgesByWay.apply(toWay));
+
+        // the search starts at *every* from edge
+        edgesByWay.apply(fromWay).forEachRemaining(from -> {
+            EdgeIteratorState edge = baseGraph.getEdgeIteratorState(from.value, Integer.MIN_VALUE);
+            explore(viaEdgesForViaWays, toEdges, edge.getBaseNode(), 0, IntArrayList.from(edge.getEdge(), edge.getBaseNode()), solutions);
+            explore(viaEdgesForViaWays, toEdges, edge.getAdjNode(), 0, IntArrayList.from(edge.getEdge(), edge.getAdjNode()), solutions);
+        });
+    }
+
+    private void explore(IntArrayList viaEdgesForViaWays, IntArrayList toEdges, int node, int viaCount, IntArrayList curr, List<IntArrayList> solutions) {
+        if (viaCount == viaEdgesForViaWays.size()) {
+            for (IntCursor to : toEdges) {
+                if (baseGraph.isAdjacentToNode(to.value, node)) {
+                    IntArrayList solution = new IntArrayList(curr);
+                    solution.add(to.value);
+                    solutions.add(solution);
+                }
+            }
+            return;
+        }
+        for (int i = 0; i < viaEdgesForViaWays.size(); i++) {
+            int viaEdge = viaEdgesForViaWays.get(i);
+            if (viaEdge < 0) continue;
+            if (baseGraph.isAdjacentToNode(viaEdge, node)) {
+                int otherNode = baseGraph.getOtherNode(viaEdge, node);
+                curr.add(viaEdge, otherNode);
+                // every via edge must only be used once, but instead of removing it we just set it to -1
+                viaEdgesForViaWays.set(i, -1);
+                explore(viaEdgesForViaWays, toEdges, otherNode, viaCount + 1, curr, solutions);
+                // backtrack
+                curr.elementsCount -= 2;
+                viaEdgesForViaWays.set(i, viaEdge);
+            }
+        }
+    }
+
+    private static IntArrayList listFromIterator(Iterator<IntCursor> iterator) {
+        IntArrayList result = new IntArrayList();
+        iterator.forEachRemaining(c -> result.add(c.value));
+        return result;
+    }
+
+    public static class EdgeResult {
+        private final IntArrayList fromEdges;
+        private final IntArrayList viaEdges;
+        private final IntArrayList toEdges;
+        private final IntArrayList nodes;
+
+        public EdgeResult(int numFrom, int numVia, int numTo) {
+            fromEdges = new IntArrayList(numFrom);
+            viaEdges = new IntArrayList(numVia);
+            toEdges = new IntArrayList(numTo);
+            nodes = new IntArrayList(numVia + 1);
+        }
+
+        public IntArrayList getFromEdges() {
+            return fromEdges;
+        }
+
+        public IntArrayList getViaEdges() {
+            return viaEdges;
+        }
+
+        public IntArrayList getToEdges() {
+            return toEdges;
+        }
+
+        /**
+         * All the intermediate nodes, i.e. for an edge chain like this:
+         * <pre>
+         *   a   b   c   d
+         * 0---1---2---3---4
+         * </pre>
+         * where 'a' is the from-edge and 'd' is the to-edge this will be [1,2,3]
+         */
+        public IntArrayList getNodes() {
+            return nodes;
+        }
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/WayToEdgesMap.java b/core/src/main/java/com/graphhopper/reader/osm/WayToEdgesMap.java
new file mode 100644
index 000000000..fffff814c
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/reader/osm/WayToEdgesMap.java
@@ -0,0 +1,94 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+import com.carrotsearch.hppc.IntArrayList;
+import com.carrotsearch.hppc.LongIntMap;
+import com.carrotsearch.hppc.LongIntScatterMap;
+import com.carrotsearch.hppc.cursors.IntCursor;
+
+import java.util.Iterator;
+
+import static java.util.Collections.emptyIterator;
+
+/**
+ * This map can store multiple edges (int) for each way ID (long). All way-edge pairs with the same way must be inserted
+ * consecutively. This allows us to simply store all edges in an array along with a mapping between the ways and the
+ * position of the associated edges in this array.
+ */
+public class WayToEdgesMap {
+    private static final int RESERVED = -1;
+    private final LongIntMap offsetIndexByWay = new LongIntScatterMap();
+    private final IntArrayList offsets = new IntArrayList();
+    private final IntArrayList edges = new IntArrayList();
+    private long lastWay = -1;
+
+    /**
+     * We need to reserve a way before we can put the associated edges into the map.
+     * This way we can define a set of keys/ways for which we shall add edges later.
+     */
+    public void reserve(long way) {
+        offsetIndexByWay.put(way, RESERVED);
+    }
+
+    public void putIfReserved(long way, int edge) {
+        if (edge < 0)
+            throw new IllegalArgumentException("edge must be >= 0, but was: " + edge);
+        if (way != lastWay) {
+            int idx = offsetIndexByWay.indexOf(way);
+            if (idx < 0)
+                // not reserved yet
+                return;
+            if (offsetIndexByWay.indexGet(idx) != RESERVED)
+                // already taken
+                throw new IllegalArgumentException("You need to add all edges for way: " + way + " consecutively");
+            offsetIndexByWay.indexReplace(idx, offsets.size());
+            offsets.add(this.edges.size());
+            lastWay = way;
+        }
+        this.edges.add(edge);
+    }
+
+    public Iterator<IntCursor> getEdges(long way) {
+        int idx = offsetIndexByWay.indexOf(way);
+        if (idx < 0)
+            return emptyIterator();
+        int offsetIndex = offsetIndexByWay.indexGet(idx);
+        if (offsetIndex == RESERVED)
+            // we reserved this, but did not put a value later
+            return emptyIterator();
+        int offsetBegin = offsets.get(offsetIndex);
+        int offsetEnd = offsetIndex + 1 < offsets.size() ? offsets.get(offsetIndex + 1) : edges.size();
+        IntCursor cursor = new IntCursor();
+        cursor.index = -1;
+        return new Iterator<IntCursor>() {
+            @Override
+            public boolean hasNext() {
+                return offsetBegin + cursor.index + 1 < offsetEnd;
+            }
+
+            @Override
+            public IntCursor next() {
+                cursor.index++;
+                cursor.value = edges.get(offsetBegin + cursor.index);
+                return cursor;
+            }
+        };
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/util/OSMParsers.java b/core/src/main/java/com/graphhopper/routing/util/OSMParsers.java
index e70c1041c..82895b65a 100644
--- a/core/src/main/java/com/graphhopper/routing/util/OSMParsers.java
+++ b/core/src/main/java/com/graphhopper/routing/util/OSMParsers.java
@@ -20,13 +20,10 @@ package com.graphhopper.routing.util;
 
 import com.graphhopper.reader.ReaderRelation;
 import com.graphhopper.reader.ReaderWay;
-import com.graphhopper.reader.osm.OSMTurnRestriction;
+import com.graphhopper.reader.osm.RestrictionTagParser;
 import com.graphhopper.routing.ev.EncodedValue;
-import com.graphhopper.routing.util.parsers.OSMTurnRestrictionParser;
 import com.graphhopper.routing.util.parsers.RelationTagParser;
 import com.graphhopper.routing.util.parsers.TagParser;
-import com.graphhopper.routing.util.parsers.TurnCostParser;
-import com.graphhopper.storage.Graph;
 import com.graphhopper.storage.IntsRef;
 
 import java.util.ArrayList;
@@ -37,18 +34,19 @@ public class OSMParsers {
     private final List<TagParser> wayTagParsers;
     private final List<VehicleTagParser> vehicleTagParsers;
     private final List<RelationTagParser> relationTagParsers;
-    private final List<TurnCostParser> turnCostParsers;
+    private final List<RestrictionTagParser> restrictionTagParsers;
     private final EncodedValue.InitializerConfig relConfig = new EncodedValue.InitializerConfig();
 
     public OSMParsers() {
         this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
     }
 
-    public OSMParsers(List<TagParser> wayTagParsers, List<VehicleTagParser> vehicleTagParsers, List<RelationTagParser> relationTagParsers, List<TurnCostParser> turnCostParsers) {
+    public OSMParsers(List<TagParser> wayTagParsers, List<VehicleTagParser> vehicleTagParsers,
+                      List<RelationTagParser> relationTagParsers, List<RestrictionTagParser> restrictionTagParsers) {
         this.wayTagParsers = wayTagParsers;
         this.vehicleTagParsers = vehicleTagParsers;
         this.relationTagParsers = relationTagParsers;
-        this.turnCostParsers = turnCostParsers;
+        this.restrictionTagParsers = restrictionTagParsers;
     }
 
     public OSMParsers addWayTagParser(TagParser tagParser) {
@@ -58,8 +56,9 @@ public class OSMParsers {
 
     public OSMParsers addVehicleTagParser(VehicleTagParser vehicleTagParser) {
         vehicleTagParsers.add(vehicleTagParser);
-        if (vehicleTagParser.supportsTurnCosts())
-            turnCostParsers.add(new OSMTurnRestrictionParser(vehicleTagParser.getAccessEnc(), vehicleTagParser.getTurnCostEnc(), vehicleTagParser.getRestrictions()));
+        if (vehicleTagParser.supportsTurnCosts()) {
+            restrictionTagParsers.add(new RestrictionTagParser(vehicleTagParser.getRestrictions(), vehicleTagParser.getTurnCostEnc()));
+        }
         return this;
     }
 
@@ -68,8 +67,8 @@ public class OSMParsers {
         return this;
     }
 
-    public OSMParsers addTurnCostTagParser(TurnCostParser turnCostParser) {
-        turnCostParsers.add(turnCostParser);
+    public OSMParsers addRestrictionTagParser(RestrictionTagParser restrictionTagParser) {
+        restrictionTagParsers.add(restrictionTagParser);
         return this;
     }
 
@@ -94,10 +93,6 @@ public class OSMParsers {
         return edgeFlags;
     }
 
-    public void handleTurnRestrictionTags(OSMTurnRestriction turnRestriction, TurnCostParser.ExternalInternalMap map, Graph graph) {
-        turnCostParsers.forEach(t -> t.handleTurnRestrictionTags(turnRestriction, map, graph));
-    }
-
     public IntsRef createRelationFlags() {
         int requiredInts = relConfig.getRequiredInts();
         if (requiredInts > 2)
@@ -108,4 +103,8 @@ public class OSMParsers {
     public List<VehicleTagParser> getVehicleTagParsers() {
         return vehicleTagParsers;
     }
+
+    public List<RestrictionTagParser> getRestrictionTagParsers() {
+        return restrictionTagParsers;
+    }
 }
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMTurnRestrictionParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMTurnRestrictionParser.java
deleted file mode 100644
index 09256aaa6..000000000
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMTurnRestrictionParser.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- *  Licensed to GraphHopper GmbH under one or more contributor
- *  license agreements. See the NOTICE file distributed with this work for
- *  additional information regarding copyright ownership.
- *
- *  GraphHopper GmbH licenses this file to you under the Apache License,
- *  Version 2.0 (the "License"); you may not use this file except in
- *  compliance with the License. You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package com.graphhopper.routing.util.parsers;
-
-import com.graphhopper.reader.osm.OSMTurnRestriction;
-import com.graphhopper.routing.ev.BooleanEncodedValue;
-import com.graphhopper.routing.ev.DecimalEncodedValue;
-import com.graphhopper.routing.util.AccessFilter;
-import com.graphhopper.storage.Graph;
-import com.graphhopper.storage.TurnCostStorage;
-import com.graphhopper.util.EdgeExplorer;
-import com.graphhopper.util.EdgeIterator;
-
-import java.util.List;
-
-/**
- * This parser takes the turn restrictions from OSM (OSM does not provide turn costs, but only restrictions) and creates the appropriate turn costs (with value infinity)
- */
-public class OSMTurnRestrictionParser implements TurnCostParser {
-    private final BooleanEncodedValue accessEnc;
-    private final DecimalEncodedValue turnCostEnc;
-    private final List<String> restrictions;
-    private EdgeExplorer cachedOutExplorer, cachedInExplorer;
-
-    public OSMTurnRestrictionParser(BooleanEncodedValue accessEnc, DecimalEncodedValue turnCostEnc, List<String> restrictions) {
-        if (restrictions.isEmpty())
-            throw new IllegalArgumentException("restrictions cannot be empty");
-        this.accessEnc = accessEnc;
-        this.turnCostEnc = turnCostEnc;
-        this.restrictions = restrictions;
-    }
-
-    @Override
-    public void handleTurnRestrictionTags(OSMTurnRestriction turnRestriction, ExternalInternalMap map, Graph graph) {
-        if (!turnRestriction.isVehicleTypeConcernedByTurnRestriction(restrictions))
-            return;
-
-        addRelationToTCStorage(turnRestriction, map, graph);
-    }
-
-    private EdgeExplorer getInExplorer(Graph graph) {
-        return cachedInExplorer == null ? cachedInExplorer = graph.createEdgeExplorer(AccessFilter.inEdges(accessEnc)) : cachedInExplorer;
-    }
-
-    private EdgeExplorer getOutExplorer(Graph graph) {
-        return cachedOutExplorer == null ? cachedOutExplorer = graph.createEdgeExplorer(AccessFilter.outEdges(accessEnc)) : cachedOutExplorer;
-    }
-
-    /**
-     * Add the specified relation to the TurnCostStorage
-     */
-    void addRelationToTCStorage(OSMTurnRestriction osmTurnRestriction, ExternalInternalMap map, Graph graph) {
-        TurnCostStorage tcs = graph.getTurnCostStorage();
-        int viaNode = map.getInternalNodeIdOfOsmNode(osmTurnRestriction.getViaOsmNodeId());
-        EdgeExplorer edgeOutExplorer = getOutExplorer(graph), edgeInExplorer = getInExplorer(graph);
-
-        try {
-            int edgeIdFrom = EdgeIterator.NO_EDGE;
-
-            // get all incoming edges and receive the edge which is defined by fromOsm
-            EdgeIterator iter = edgeInExplorer.setBaseNode(viaNode);
-
-            while (iter.next()) {
-                if (map.getOsmIdOfInternalEdge(iter.getEdge()) == osmTurnRestriction.getOsmIdFrom()) {
-                    edgeIdFrom = iter.getEdge();
-                    break;
-                }
-            }
-
-            if (!EdgeIterator.Edge.isValid(edgeIdFrom))
-                return;
-
-            // get all outgoing edges of the via node
-            iter = edgeOutExplorer.setBaseNode(viaNode);
-            // for TYPE_ONLY_* we add ALL restrictions (from, via, * ) EXCEPT the given turn
-            // for TYPE_NOT_*  we add ONE restriction  (from, via, to)
-            while (iter.next()) {
-                int edgeId = iter.getEdge();
-                long wayId = map.getOsmIdOfInternalEdge(edgeId);
-                if (edgeId != edgeIdFrom && osmTurnRestriction.getRestriction() == OSMTurnRestriction.RestrictionType.ONLY && wayId != osmTurnRestriction.getOsmIdTo()
-                        || osmTurnRestriction.getRestriction() == OSMTurnRestriction.RestrictionType.NOT && wayId == osmTurnRestriction.getOsmIdTo() && wayId >= 0) {
-                    tcs.set(turnCostEnc, edgeIdFrom, viaNode, iter.getEdge(), Double.POSITIVE_INFINITY);
-                    if (osmTurnRestriction.getRestriction() == OSMTurnRestriction.RestrictionType.NOT)
-                        break;
-                }
-            }
-        } catch (Exception e) {
-            throw new IllegalStateException("Could not build turn table entry for relation of node with osmId:" + osmTurnRestriction.getViaOsmNodeId(), e);
-        }
-    }
-
-    @Override
-    public String getName() {
-        return turnCostEnc.getName();
-    }
-
-    @Override
-    public String toString() {
-        return getName();
-    }
-}
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/RestrictionSetter.java b/core/src/main/java/com/graphhopper/routing/util/parsers/RestrictionSetter.java
new file mode 100644
index 000000000..16f22e262
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/RestrictionSetter.java
@@ -0,0 +1,200 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.routing.util.parsers;
+
+import com.carrotsearch.hppc.*;
+import com.carrotsearch.hppc.cursors.IntCursor;
+import com.graphhopper.reader.osm.GraphRestriction;
+import com.graphhopper.reader.osm.Pair;
+import com.graphhopper.reader.osm.RestrictionType;
+import com.graphhopper.routing.ev.DecimalEncodedValue;
+import com.graphhopper.storage.BaseGraph;
+import com.graphhopper.util.EdgeExplorer;
+import com.graphhopper.util.EdgeIterator;
+import com.graphhopper.util.EdgeIteratorState;
+import com.graphhopper.util.FetchMode;
+
+import java.util.List;
+
+import static com.graphhopper.reader.osm.RestrictionType.NO;
+import static com.graphhopper.reader.osm.RestrictionType.ONLY;
+
+public class RestrictionSetter {
+    private final BaseGraph baseGraph;
+    private final EdgeExplorer edgeExplorer;
+    private final IntIntMap artificialEdgesByEdges = new IntIntHashMap();
+
+    public RestrictionSetter(BaseGraph baseGraph) {
+        this.baseGraph = baseGraph;
+        this.edgeExplorer = baseGraph.createEdgeExplorer();
+    }
+
+    /**
+     * Adds all the turn restriction entries to the graph that are needed to enforce the given restrictions, for
+     * a single turn cost encoded value.
+     * Implementing via-way turn restrictions requires adding artificial edges to the graph, which is also handled here.
+     * Since we keep track of the added artificial edges here it is important to only use one RestrictionSetter instance
+     * for **all** turn restrictions and vehicle types.
+     */
+    public void setRestrictions(List<Pair<GraphRestriction, RestrictionType>> restrictions, DecimalEncodedValue turnCostEnc) {
+        // we first need to add all the artificial edges, because we might need to restrict turns between artificial
+        // edges created for different restrictions (when restrictions are overlapping)
+        addArtificialEdges(restrictions);
+        // now we can add all the via-way restrictions
+        addViaWayRestrictions(restrictions, turnCostEnc);
+        // ... and finally all the via-node restrictions
+        addViaNodeRestrictions(restrictions, turnCostEnc);
+    }
+
+    private void addArtificialEdges(List<Pair<GraphRestriction, RestrictionType>> restrictions) {
+        for (Pair<GraphRestriction, RestrictionType> p : restrictions) {
+            if (p.first.isViaWayRestriction()) {
+                if (ignoreViaWayRestriction(p)) continue;
+                int viaEdge = p.first.getViaEdges().get(0);
+                int artificialEdge = artificialEdgesByEdges.getOrDefault(viaEdge, -1);
+                if (artificialEdge < 0) {
+                    EdgeIteratorState viaEdgeState = baseGraph.getEdgeIteratorState(p.first.getViaEdges().get(0), Integer.MIN_VALUE);
+                    EdgeIteratorState artificialEdgeState = baseGraph.edge(viaEdgeState.getBaseNode(), viaEdgeState.getAdjNode())
+                            .setFlags(viaEdgeState.getFlags())
+                            .setWayGeometry(viaEdgeState.fetchWayGeometry(FetchMode.PILLAR_ONLY))
+                            .setDistance(viaEdgeState.getDistance())
+                            .setKeyValues(viaEdgeState.getKeyValues());
+                    artificialEdge = artificialEdgeState.getEdge();
+                    artificialEdgesByEdges.put(viaEdge, artificialEdge);
+                }
+            }
+        }
+    }
+
+    private void addViaWayRestrictions(List<Pair<GraphRestriction, RestrictionType>> restrictions, DecimalEncodedValue turnCostEnc) {
+        IntSet viaEdgesUsedByOnlyRestrictions = new IntHashSet();
+        for (Pair<GraphRestriction, RestrictionType> p : restrictions) {
+            if (!p.first.isViaWayRestriction()) continue;
+            if (ignoreViaWayRestriction(p)) continue;
+            final int fromEdge = p.first.getFromEdges().get(0);
+            final int viaEdge = p.first.getViaEdges().get(0);
+            final int toEdge = p.first.getToEdges().get(0);
+            final int artificialVia = artificialEdgesByEdges.getOrDefault(viaEdge, viaEdge);
+            if (artificialVia == viaEdge)
+                throw new IllegalArgumentException("There should be an artificial edge for every via edge of a way restriction");
+            if (p.second == ONLY && !viaEdgesUsedByOnlyRestrictions.add(viaEdge))
+                throw new IllegalStateException("We cannot deal with 'only' via-way restrictions that use the same via edges");
+            final int artificialFrom = artificialEdgesByEdges.getOrDefault(fromEdge, fromEdge);
+            final int artificialTo = artificialEdgesByEdges.getOrDefault(toEdge, toEdge);
+            final int fromToViaNode = p.first.getViaNodes().get(0);
+            final int viaToToNode = p.first.getViaNodes().get(1);
+
+            // never turn between an artificial edge and its corresponding real edge
+            restrictTurn(turnCostEnc, artificialVia, fromToViaNode, viaEdge);
+            restrictTurn(turnCostEnc, viaEdge, fromToViaNode, artificialVia);
+            restrictTurn(turnCostEnc, artificialVia, viaToToNode, viaEdge);
+            restrictTurn(turnCostEnc, viaEdge, viaToToNode, artificialVia);
+
+            if (p.second == NO) {
+                // This is how we implement via-way NO restrictions: we deny turning from the from-edge onto the via-edge,
+                // but allow turning onto the artificial edge instead. Then we deny turning from the artificial edge onto
+                // the to edge.
+                restrictTurn(turnCostEnc, fromEdge, fromToViaNode, viaEdge);
+                restrictTurn(turnCostEnc, artificialVia, viaToToNode, toEdge);
+            } else if (p.second == ONLY) {
+                // For via-way ONLY restrictions we have to turn from the from-edge onto the via-edge and from the via-edge
+                // onto the to-edge, but only if we actually start at the from-edge. Therefore we enforce turning onto
+                // the artificial via-edge when we are coming from the from-edge and only allow turning onto the to-edge
+                // when coming from the artificial via-edge.
+                EdgeIterator iter = edgeExplorer.setBaseNode(fromToViaNode);
+                while (iter.next())
+                    if (iter.getEdge() != fromEdge && iter.getEdge() != artificialVia)
+                        restrictTurn(turnCostEnc, fromEdge, fromToViaNode, iter.getEdge());
+                iter = edgeExplorer.setBaseNode(viaToToNode);
+                while (iter.next())
+                    if (iter.getEdge() != artificialVia && iter.getEdge() != toEdge)
+                        restrictTurn(turnCostEnc, artificialVia, viaToToNode, iter.getEdge());
+            } else {
+                throw new IllegalArgumentException("Unexpected restriction type: " + p.second);
+            }
+
+            // this is important for overlapping restrictions
+            if (artificialFrom != fromEdge)
+                restrictTurn(turnCostEnc, artificialFrom, fromToViaNode, artificialVia);
+            if (artificialTo != toEdge)
+                restrictTurn(turnCostEnc, artificialVia, viaToToNode, artificialTo);
+        }
+    }
+
+    private void addViaNodeRestrictions(List<Pair<GraphRestriction, RestrictionType>> restrictions, DecimalEncodedValue turnCostEnc) {
+        for (Pair<GraphRestriction, RestrictionType> p : restrictions) {
+            if (p.first.isViaWayRestriction()) continue;
+            final int viaNode = p.first.getViaNodes().get(0);
+            for (IntCursor fromEdgeCursor : p.first.getFromEdges()) {
+                for (IntCursor toEdgeCursor : p.first.getToEdges()) {
+                    final int fromEdge = fromEdgeCursor.value;
+                    final int toEdge = toEdgeCursor.value;
+                    final int artificialFrom = artificialEdgesByEdges.getOrDefault(fromEdge, fromEdge);
+                    final int artificialTo = artificialEdgesByEdges.getOrDefault(toEdge, toEdge);
+                    if (p.second == NO) {
+                        restrictTurn(turnCostEnc, fromEdge, viaNode, toEdge);
+                        // we also need to restrict this term in case there are artificial edges for the from- and/or to-edge
+                        if (artificialFrom != fromEdge)
+                            restrictTurn(turnCostEnc, artificialFrom, viaNode, toEdge);
+                        if (artificialTo != toEdge)
+                            restrictTurn(turnCostEnc, fromEdge, viaNode, artificialTo);
+                        if (artificialFrom != fromEdge && artificialTo != toEdge)
+                            restrictTurn(turnCostEnc, artificialFrom, viaNode, artificialTo);
+                    } else if (p.second == ONLY) {
+                        // we need to restrict all turns except the one, but that also means not restricting the
+                        // artificial counterparts of these turns, if they exist.
+                        // we do not explicitly restrict the U-turn from the from-edge back to the from-edge though.
+                        EdgeIterator iter = edgeExplorer.setBaseNode(viaNode);
+                        while (iter.next()) {
+                            if (iter.getEdge() != fromEdge && iter.getEdge() != toEdge && iter.getEdge() != artificialTo)
+                                restrictTurn(turnCostEnc, fromEdge, viaNode, iter.getEdge());
+                            // and the same for the artificial edge belonging to the from-edge if it exists
+                            if (fromEdge != artificialFrom && iter.getEdge() != artificialFrom && iter.getEdge() != toEdge && iter.getEdge() != artificialTo)
+                                restrictTurn(turnCostEnc, artificialFrom, viaNode, iter.getEdge());
+                        }
+                    } else {
+                        throw new IllegalArgumentException("Unexpected restriction type: " + p.second);
+                    }
+                }
+            }
+        }
+    }
+
+    public IntIntMap getArtificialEdgesByEdges() {
+        return artificialEdgesByEdges;
+    }
+
+    private void restrictTurn(DecimalEncodedValue turnCostEnc, int fromEdge, int viaNode, int toEdge) {
+        if (fromEdge < 0 || toEdge < 0 || viaNode < 0)
+            throw new IllegalArgumentException("from/toEdge and viaNode must be >= 0");
+        baseGraph.getTurnCostStorage().set(turnCostEnc, fromEdge, viaNode, toEdge, Double.POSITIVE_INFINITY);
+    }
+
+    private static boolean ignoreViaWayRestriction(Pair<GraphRestriction, RestrictionType> p) {
+        // todo: how frequent are these?
+        if (p.first.getViaEdges().size() > 1)
+            // no multi-restrictions yet
+            return true;
+        if (p.first.getFromEdges().size() > 1 || p.first.getToEdges().size() > 1)
+            // no multi-from or -to yet
+            return true;
+        return false;
+    }
+
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/TurnCostParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/TurnCostParser.java
deleted file mode 100644
index 455026ecb..000000000
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/TurnCostParser.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- *  Licensed to GraphHopper GmbH under one or more contributor
- *  license agreements. See the NOTICE file distributed with this work for
- *  additional information regarding copyright ownership.
- *
- *  GraphHopper GmbH licenses this file to you under the Apache License,
- *  Version 2.0 (the "License"); you may not use this file except in
- *  compliance with the License. You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package com.graphhopper.routing.util.parsers;
-
-import com.graphhopper.reader.osm.OSMTurnRestriction;
-import com.graphhopper.storage.Graph;
-
-/**
- * This interface serves the purpose of converting relation flags into turn cost information. Unlike RelationTagParser
- * it can be assumed that the graph topology is already intact when handleTurnRestrictionTags is called.
- */
-public interface TurnCostParser {
-    String getName();
-
-    void handleTurnRestrictionTags(OSMTurnRestriction turnRestriction, ExternalInternalMap map, Graph graph);
-
-    /**
-     * This map associates the internal GraphHopper nodes IDs with external IDs (OSM) and similarly for the edge IDs
-     * required to write the turn costs. Returns -1 if there is no entry for the given OSM ID.
-     */
-    interface ExternalInternalMap {
-        int getInternalNodeIdOfOsmNode(long nodeOsmId);
-
-        long getOsmIdOfInternalEdge(int edgeId);
-    }
-}
diff --git a/core/src/main/java/com/graphhopper/storage/TurnCostStorage.java b/core/src/main/java/com/graphhopper/storage/TurnCostStorage.java
index a05c086c4..b041b9e3d 100644
--- a/core/src/main/java/com/graphhopper/storage/TurnCostStorage.java
+++ b/core/src/main/java/com/graphhopper/storage/TurnCostStorage.java
@@ -45,8 +45,8 @@ public class TurnCostStorage {
     private static final int TC_NEXT = 12;
     private static final int BYTES_PER_ENTRY = 16;
 
-    private BaseGraph baseGraph;
-    private DataAccess turnCosts;
+    private final BaseGraph baseGraph;
+    private final DataAccess turnCosts;
     private int turnCostsCount;
 
     public TurnCostStorage(BaseGraph baseGraph, DataAccess turnCosts) {
diff --git a/core/src/main/java/com/graphhopper/util/GHUtility.java b/core/src/main/java/com/graphhopper/util/GHUtility.java
index 1dddec450..8eb5ea48d 100644
--- a/core/src/main/java/com/graphhopper/util/GHUtility.java
+++ b/core/src/main/java/com/graphhopper/util/GHUtility.java
@@ -336,6 +336,11 @@ public class GHUtility {
      */
     public static Graph sortDFS(Graph g, Graph sortedGraph) {
         if (g.getTurnCostStorage() != null) {
+            // not only would we have to sort the turn cost storage when we re-sort the graph, but we'd also have to make
+            // sure that the location index always snaps to real edges and not the corresponding artificial edge that we
+            // introduced to deal with via-way restrictions. Without sorting this works automatically because the real
+            // edges use lower edge ids. Otherwise we'd probably have to use some kind of is_artificial flag for each
+            // edge.
             throw new IllegalArgumentException("Sorting the graph is currently not supported in the presence of turn costs");
         }
         int nodes = g.getNodes();
diff --git a/map-matching/src/main/java/com/graphhopper/matching/MapMatching.java b/map-matching/src/main/java/com/graphhopper/matching/MapMatching.java
index 6dcc5ec11..e9fd1038f 100644
--- a/map-matching/src/main/java/com/graphhopper/matching/MapMatching.java
+++ b/map-matching/src/main/java/com/graphhopper/matching/MapMatching.java
@@ -427,6 +427,9 @@ public class MapMatching {
                 }
             }
         }
+        if (qe == null) {
+            throw new IllegalArgumentException("Sequence is broken for submitted track at initial time step.");
+        }
         if (qe.timeStep != timeSteps.size() - 1) {
             throw new IllegalArgumentException("Sequence is broken for submitted track at time step "
                     + qe.timeStep + ". observation:" + qe.state.getEntry());


Test patch:
diff --git a/core/files/test_simple_turncosts.osm.xml b/core/files/test_simple_turncosts.osm.xml
index b98915926..220e19e78 100644
--- a/core/files/test_simple_turncosts.osm.xml
+++ b/core/files/test_simple_turncosts.osm.xml
@@ -11,6 +11,12 @@
 
     <way id="1" version="1">
         <nd ref="1"/>
+        <nd ref="2"/>
+        <tag k="name" v="str"/>
+        <tag k="highway" v="secondary"/>
+        <tag k="oneway" v="yes"/>
+    </way>
+    <way id="111" version="1">
         <nd ref="2"/>
         <nd ref="3"/>
         <nd ref="4"/>
diff --git a/core/src/test/java/com/graphhopper/reader/osm/ExtractMembersTest.java b/core/src/test/java/com/graphhopper/reader/osm/ExtractMembersTest.java
new file mode 100644
index 000000000..2a9c1c315
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/reader/osm/ExtractMembersTest.java
@@ -0,0 +1,116 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+import com.carrotsearch.hppc.LongArrayList;
+import com.graphhopper.reader.ReaderRelation;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static com.graphhopper.reader.ReaderElement.Type.NODE;
+import static com.graphhopper.reader.ReaderElement.Type.WAY;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ExtractMembersTest {
+    private ReaderRelation relation;
+
+    @BeforeEach
+    void setup() {
+        relation = new ReaderRelation(0);
+        relation.setTag("type", "restriction");
+    }
+
+    @Test
+    void simpleViaNode() throws OSMRestrictionException {
+        relation.add(new ReaderRelation.Member(WAY, 1, "from"));
+        relation.add(new ReaderRelation.Member(NODE, 2, "via"));
+        relation.add(new ReaderRelation.Member(WAY, 3, "to"));
+        RestrictionMembers restrictionMembers = RestrictionConverter.extractMembers(relation);
+        assertEquals(LongArrayList.from(1), restrictionMembers.getFromWays());
+        assertEquals(2, restrictionMembers.getViaOSMNode());
+        assertEquals(LongArrayList.from(3), restrictionMembers.getToWays());
+        assertNull(restrictionMembers.getViaWays());
+    }
+
+    @Test
+    void noVia() {
+        relation.add(new ReaderRelation.Member(WAY, 1, "from"));
+        relation.add(new ReaderRelation.Member(WAY, 2, "to"));
+        OSMRestrictionException e = assertThrows(OSMRestrictionException.class, () -> RestrictionConverter.extractMembers(relation));
+        assertTrue(e.getMessage().contains("has no member with role 'via'"), e.getMessage());
+    }
+
+    @Test
+    void multipleViaNodes() {
+        relation.add(new ReaderRelation.Member(WAY, 1, "from"));
+        relation.add(new ReaderRelation.Member(NODE, 2, "via"));
+        relation.add(new ReaderRelation.Member(NODE, 3, "via"));
+        relation.add(new ReaderRelation.Member(WAY, 4, "to"));
+        OSMRestrictionException e = assertThrows(OSMRestrictionException.class, () -> RestrictionConverter.extractMembers(relation));
+        assertTrue(e.getMessage().contains("has multiple members with role 'via' and type 'node'"), e.getMessage());
+    }
+
+    @Test
+    void multipleFromButNotNoEntry() {
+        relation.add(new ReaderRelation.Member(WAY, 1, "from"));
+        relation.add(new ReaderRelation.Member(WAY, 2, "from"));
+        relation.add(new ReaderRelation.Member(NODE, 3, "via"));
+        relation.add(new ReaderRelation.Member(WAY, 4, "to"));
+        OSMRestrictionException e = assertThrows(OSMRestrictionException.class, () -> RestrictionConverter.extractMembers(relation));
+        assertTrue(e.getMessage().contains("has multiple members with role 'from' even though it is not a 'no_entry' restriction"), e.getMessage());
+    }
+
+    @Test
+    void noEntry() throws OSMRestrictionException {
+        relation.setTag("restriction", "no_entry");
+        relation.add(new ReaderRelation.Member(WAY, 1, "from"));
+        relation.add(new ReaderRelation.Member(WAY, 2, "from"));
+        relation.add(new ReaderRelation.Member(NODE, 3, "via"));
+        relation.add(new ReaderRelation.Member(WAY, 4, "to"));
+        RestrictionMembers res = RestrictionConverter.extractMembers(relation);
+        assertEquals(LongArrayList.from(1, 2), res.getFromWays());
+        assertEquals(3, res.getViaOSMNode());
+        assertEquals(LongArrayList.from(4), res.getToWays());
+    }
+
+    @Test
+    void multipleToButNoNoExit() {
+        relation.setTag("restriction", "no_left_turn");
+        relation.add(new ReaderRelation.Member(WAY, 1, "from"));
+        relation.add(new ReaderRelation.Member(NODE, 2, "via"));
+        relation.add(new ReaderRelation.Member(WAY, 3, "to"));
+        relation.add(new ReaderRelation.Member(WAY, 4, "to"));
+        OSMRestrictionException e = assertThrows(OSMRestrictionException.class, () -> RestrictionConverter.extractMembers(relation));
+        assertTrue(e.getMessage().contains("has multiple members with role 'to' even though it is not a 'no_exit' restriction"), e.getMessage());
+    }
+
+    @Test
+    void noExit() throws OSMRestrictionException {
+        relation.setTag("restriction", "no_exit");
+        relation.add(new ReaderRelation.Member(WAY, 1, "from"));
+        relation.add(new ReaderRelation.Member(NODE, 2, "via"));
+        relation.add(new ReaderRelation.Member(WAY, 3, "to"));
+        relation.add(new ReaderRelation.Member(WAY, 4, "to"));
+        RestrictionMembers res = RestrictionConverter.extractMembers(relation);
+        assertEquals(LongArrayList.from(1), res.getFromWays());
+        assertEquals(2, res.getViaOSMNode());
+        assertEquals(LongArrayList.from(3, 4), res.getToWays());
+    }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/reader/osm/OSMReaderTest.java b/core/src/test/java/com/graphhopper/reader/osm/OSMReaderTest.java
index 83fca298d..5759495bc 100644
--- a/core/src/test/java/com/graphhopper/reader/osm/OSMReaderTest.java
+++ b/core/src/test/java/com/graphhopper/reader/osm/OSMReaderTest.java
@@ -575,7 +575,8 @@ public class OSMReaderTest {
         assertTrue(tcStorage.get(carTCEnc, edge11_14, n11, edge10_11) == 0);
         assertTrue(tcStorage.get(bikeTCEnc, edge11_14, n11, edge10_11) == 0);
 
-        assertTrue(tcStorage.get(carTCEnc, edge10_11, n11, edge11_14) == 0);
+        // the turn is restricted for car even though it turns into a one-way, but we treat this separately now
+        assertTrue(tcStorage.get(carTCEnc, edge10_11, n11, edge11_14) > 0);
         assertTrue(tcStorage.get(bikeTCEnc, edge10_11, n11, edge11_14) > 0);
     }
 
diff --git a/core/src/test/java/com/graphhopper/reader/osm/OSMRestrictionRelationParserTest.java b/core/src/test/java/com/graphhopper/reader/osm/OSMRestrictionRelationParserTest.java
deleted file mode 100644
index f3c291c9c..000000000
--- a/core/src/test/java/com/graphhopper/reader/osm/OSMRestrictionRelationParserTest.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- *  Licensed to GraphHopper GmbH under one or more contributor
- *  license agreements. See the NOTICE file distributed with this work for
- *  additional information regarding copyright ownership.
- *
- *  GraphHopper GmbH licenses this file to you under the Apache License,
- *  Version 2.0 (the "License"); you may not use this file except in
- *  compliance with the License. You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.graphhopper.reader.osm;
-
-import com.graphhopper.reader.ReaderElement;
-import com.graphhopper.reader.ReaderRelation;
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.graphhopper.reader.osm.OSMRestrictionRelationParser.createTurnRestrictions;
-import static com.graphhopper.reader.osm.OSMTurnRestriction.RestrictionType.NOT;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class OSMRestrictionRelationParserTest {
-    @Test
-    public void turnRestrictionsRestrictedToCertainVehicles() {
-        {
-            ReaderRelation rel = createRestrictionRelation();
-            List<String> warnings = new ArrayList<>();
-            assertTrue(createTurnRestrictions(rel, warnings::add).isEmpty());
-            assertEquals(1, warnings.size());
-            assertTrue(warnings.get(0).contains("Restriction relation 0 neither has a 'restriction' nor 'restriction:' tags"), warnings.get(0));
-        }
-        {
-            ReaderRelation rel = createRestrictionRelation();
-            rel.setTag("restriction:bicycle", "no_right_turn");
-            rel.setTag("except", "bus");
-            List<String> warnings = new ArrayList<>();
-            assertTrue(createTurnRestrictions(rel, warnings::add).isEmpty());
-            assertEquals(1, warnings.size());
-            assertTrue(warnings.get(0).contains("Restriction relation 0 has an 'except', but no 'restriction' or 'restriction:conditional' tag"), warnings.get(0));
-        }
-        {
-            ReaderRelation rel = createRestrictionRelation();
-            rel.setTag("restriction:hgv:conditional", "no_right_turn @ (weight > 3.5)");
-            rel.setTag("except", "bus");
-            List<String> warnings = new ArrayList<>();
-            assertTrue(createTurnRestrictions(rel, warnings::add).isEmpty());
-            assertEquals(1, warnings.size());
-            assertTrue(warnings.get(0).contains("Restriction relation 0 has an 'except', but no 'restriction' or 'restriction:conditional' tag"), warnings.get(0));
-        }
-        {
-            ReaderRelation rel = createRestrictionRelation();
-            rel.setTag("restriction:conditional", "no_right_turn @ (weight > 3.5)");
-            rel.setTag("except", "bus");
-            List<String> warnings = new ArrayList<>();
-            // we do not handle conditional restrictions yet, but no warning for except+restriction:conditional, because that would make sense
-            assertTrue(createTurnRestrictions(rel, warnings::add).isEmpty());
-            assertEquals(0, warnings.size());
-        }
-        {
-            // restriction and restriction:vehicle
-            ReaderRelation rel = createRestrictionRelation();
-            rel.setTag("restriction", "no_left_turn");
-            rel.setTag("restriction:bus", "no_left_turn");
-            List<String> warnings = new ArrayList<>();
-            assertTrue(createTurnRestrictions(rel, warnings::add).isEmpty());
-            assertEquals(1, warnings.size());
-            assertTrue(warnings.get(0).contains("Restriction relation 0 has a 'restriction' tag, but also 'restriction:' tags"), warnings.get(0));
-        }
-        {
-            ReaderRelation rel = createRestrictionRelation();
-            // this could happen for real, or at least it wouldn't be nonsensical. we ignore give_way so far, but do not
-            // ignore or warn about the entire relation
-            rel.setTag("restriction:bicycle", "give_way");
-            rel.setTag("restriction", "no_left_turn");
-            List<String> warnings = new ArrayList<>();
-            List<OSMTurnRestriction> turnRestrictions = createTurnRestrictions(rel, warnings::add);
-            assertEquals(1, turnRestrictions.size());
-            assertTrue(warnings.isEmpty());
-            assertEquals("", turnRestrictions.get(0).getVehicleTypeRestricted());
-            assertTrue(turnRestrictions.get(0).getVehicleTypesExcept().isEmpty());
-        }
-        {
-            ReaderRelation rel = createRestrictionRelation();
-            // So far we are ignoring conditional restrictions, even though for example weight restrictions could
-            // be interesting. But shouldn't they simply be mapped using restriction:hgv?
-            rel.setTag("restriction:conditional", "no_left_turn @ (weight > 3.5)");
-            List<String> warnings = new ArrayList<>();
-            assertTrue(createTurnRestrictions(rel, warnings::add).isEmpty());
-            assertEquals(0, warnings.size());
-        }
-        {
-            ReaderRelation rel = createRestrictionRelation();
-            rel.setTag("restriction", "no_left_turn");
-            rel.setTag("except", "bus");
-            List<String> warnings = new ArrayList<>();
-            List<OSMTurnRestriction> turnRestrictions = createTurnRestrictions(rel, warnings::add);
-            assertEquals(1, turnRestrictions.size());
-            assertEquals("", turnRestrictions.get(0).getVehicleTypeRestricted());
-            assertEquals(1, turnRestrictions.get(0).getVehicleTypesExcept().size());
-            assertEquals("bus", turnRestrictions.get(0).getVehicleTypesExcept().get(0));
-            assertEquals(0, warnings.size());
-        }
-        {
-            ReaderRelation rel = createRestrictionRelation();
-            rel.setTag("restriction:motorcar", "no_left_turn");
-            List<String> warnings = new ArrayList<>();
-            List<OSMTurnRestriction> turnRestrictions = createTurnRestrictions(rel, warnings::add);
-            assertEquals(1, turnRestrictions.size());
-            assertEquals("motorcar", turnRestrictions.get(0).getVehicleTypeRestricted());
-            assertTrue(turnRestrictions.get(0).getVehicleTypesExcept().isEmpty());
-            assertEquals(0, warnings.size());
-        }
-        {
-            ReaderRelation rel = createRestrictionRelation();
-            rel.setTag("restriction:motorcar", "no_left_turn");
-            rel.setTag("restriction:bus", "no_left_turn");
-            List<String> warnings = new ArrayList<>();
-            List<OSMTurnRestriction> turnRestrictions = createTurnRestrictions(rel, warnings::add);
-            assertEquals(2, turnRestrictions.size());
-            assertEquals("motorcar", turnRestrictions.get(0).getVehicleTypeRestricted());
-            assertEquals("bus", turnRestrictions.get(1).getVehicleTypeRestricted());
-            assertTrue(turnRestrictions.get(0).getVehicleTypesExcept().isEmpty());
-            assertEquals(0, warnings.size());
-        }
-    }
-
-    private static ReaderRelation createRestrictionRelation() {
-        ReaderRelation rel = new ReaderRelation(0);
-        rel.setTag("type", "restriction");
-        rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 1, "from"));
-        rel.add(new ReaderRelation.Member(ReaderElement.Type.NODE, 2, "via"));
-        rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 3, "to"));
-        return rel;
-    }
-
-    @Test
-    public void turnRestrictionsWithoutVehicleConstraints() {
-        {
-            ReaderRelation rel = createRestrictionWithoutMembers("no_left_turn");
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 1, "from"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.NODE, 2, "via"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 3, "to"));
-            List<String> warnings = new ArrayList<>();
-            List<OSMTurnRestriction> turnRestrictions = createTurnRestrictions(rel, warnings::add);
-            assertEquals(0, warnings.size());
-            assertEquals(1, turnRestrictions.size());
-            assertEquals(1, turnRestrictions.get(0).getOsmIdFrom());
-            assertEquals(2, turnRestrictions.get(0).getViaOsmNodeId());
-            assertEquals(3, turnRestrictions.get(0).getOsmIdTo());
-            assertEquals(NOT, turnRestrictions.get(0).getRestriction());
-        }
-        {
-            ReaderRelation rel = createRestrictionWithoutMembers("no_left_turn");
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 1, "from"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 2, "from"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.NODE, 3, "via"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 4, "to"));
-            List<String> warnings = new ArrayList<>();
-            List<OSMTurnRestriction> turnRestrictions = createTurnRestrictions(rel, warnings::add);
-            assertEquals(0, turnRestrictions.size());
-            assertEquals(1, warnings.size());
-            assertTrue(warnings.get(0).contains("Restriction relation 0 has multiple members with role 'from' even though it is not a 'no_entry' restriction"), warnings.get(0));
-        }
-        {
-            ReaderRelation rel = createRestrictionWithoutMembers("no_entry");
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 1, "from"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 2, "from"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.NODE, 3, "via"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 4, "to"));
-            List<String> warnings = new ArrayList<>();
-            List<OSMTurnRestriction> turnRestrictions = createTurnRestrictions(rel, warnings::add);
-            assertEquals(2, turnRestrictions.size());
-            assertEquals(1, turnRestrictions.get(0).getOsmIdFrom());
-            assertEquals(2, turnRestrictions.get(1).getOsmIdFrom());
-            turnRestrictions.forEach(t -> assertEquals(4, t.getOsmIdTo()));
-            turnRestrictions.forEach(t -> assertEquals(NOT, t.getRestriction()));
-            assertEquals(0, warnings.size());
-        }
-        {
-            ReaderRelation rel = createRestrictionWithoutMembers("no_left_turn");
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 1, "from"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.NODE, 2, "via"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 3, "to"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 4, "to"));
-            List<String> warnings = new ArrayList<>();
-            List<OSMTurnRestriction> turnRestrictions = createTurnRestrictions(rel, warnings::add);
-            assertEquals(0, turnRestrictions.size());
-            assertEquals(1, warnings.size());
-            assertTrue(warnings.get(0).contains("Restriction relation 0 has multiple members with role 'to' even though it is not a 'no_exit' restriction"), warnings.get(0));
-        }
-        {
-            ReaderRelation rel = createRestrictionWithoutMembers("no_exit");
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 1, "from"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.NODE, 2, "via"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 3, "to"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 4, "to"));
-            List<String> warnings = new ArrayList<>();
-            List<OSMTurnRestriction> turnRestrictions = createTurnRestrictions(rel, warnings::add);
-            assertEquals(2, turnRestrictions.size());
-            assertEquals(3, turnRestrictions.get(0).getOsmIdTo());
-            assertEquals(4, turnRestrictions.get(1).getOsmIdTo());
-            turnRestrictions.forEach(t -> assertEquals(1, t.getOsmIdFrom()));
-            turnRestrictions.forEach(t -> assertEquals(NOT, t.getRestriction()));
-            assertEquals(0, warnings.size());
-        }
-        {
-            ReaderRelation rel = createRestrictionWithoutMembers("no_left_turn");
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 1, "from"));
-            rel.add(new ReaderRelation.Member(ReaderElement.Type.WAY, 2, "to"));
-            List<String> warnings = new ArrayList<>();
-            List<OSMTurnRestriction> turnRestrictions = createTurnRestrictions(rel, warnings::add);
-            assertEquals(0, turnRestrictions.size());
-            assertEquals(1, warnings.size());
-            assertTrue(warnings.get(0).contains("Restriction relation 0 has no member with role 'via'"), warnings.get(0));
-        }
-    }
-
-    private static ReaderRelation createRestrictionWithoutMembers(String restriction) {
-        ReaderRelation rel = new ReaderRelation(0);
-        rel.setTag("type", "restriction");
-        rel.setTag("restriction", restriction);
-        return rel;
-    }
-
-}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/reader/osm/OSMTurnRestrictionTest.java b/core/src/test/java/com/graphhopper/reader/osm/OSMTurnRestrictionTest.java
deleted file mode 100644
index 77ea19165..000000000
--- a/core/src/test/java/com/graphhopper/reader/osm/OSMTurnRestrictionTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- *  Licensed to GraphHopper GmbH under one or more contributor
- *  license agreements. See the NOTICE file distributed with this work for
- *  additional information regarding copyright ownership.
- *
- *  GraphHopper GmbH licenses this file to you under the Apache License,
- *  Version 2.0 (the "License"); you may not use this file except in
- *  compliance with the License. You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package com.graphhopper.reader.osm;
-
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author Peter Karich
- */
-public class OSMTurnRestrictionTest {
-    @Test
-    public void testAcceptsTurnRestriction() {
-        List<String> vehicleTypes = new ArrayList<>(Arrays.asList("motorcar", "motor_vehicle", "vehicle"));
-        List<String> vehicleTypesExcept = new ArrayList<>();
-        OSMTurnRestriction osmTurnRestriction = new OSMTurnRestriction(1, 1, 1, OSMTurnRestriction.RestrictionType.NOT);
-        assertTrue(osmTurnRestriction.isVehicleTypeConcernedByTurnRestriction(vehicleTypes));
-
-        vehicleTypesExcept.add("bus");
-        osmTurnRestriction.setVehicleTypesExcept(vehicleTypesExcept);
-        assertTrue(osmTurnRestriction.isVehicleTypeConcernedByTurnRestriction(vehicleTypes));
-
-        vehicleTypesExcept.clear();
-        vehicleTypesExcept.add("vehicle");
-        osmTurnRestriction.setVehicleTypesExcept(vehicleTypesExcept);
-        assertFalse(osmTurnRestriction.isVehicleTypeConcernedByTurnRestriction(vehicleTypes));
-
-        vehicleTypesExcept.clear();
-        vehicleTypesExcept.add("motor_vehicle");
-        vehicleTypesExcept.add("vehicle");
-        osmTurnRestriction.setVehicleTypesExcept(vehicleTypesExcept);
-        assertFalse(osmTurnRestriction.isVehicleTypeConcernedByTurnRestriction(vehicleTypes));
-
-        vehicleTypesExcept.clear();
-        osmTurnRestriction.setVehicleTypeRestricted("bus");
-        osmTurnRestriction.setVehicleTypesExcept(vehicleTypesExcept);
-        assertFalse(osmTurnRestriction.isVehicleTypeConcernedByTurnRestriction(vehicleTypes));
-
-        osmTurnRestriction.setVehicleTypeRestricted("vehicle");
-        assertTrue(osmTurnRestriction.isVehicleTypeConcernedByTurnRestriction(vehicleTypes));
-    }
-}
diff --git a/core/src/test/java/com/graphhopper/reader/osm/RestrictionTagParserTest.java b/core/src/test/java/com/graphhopper/reader/osm/RestrictionTagParserTest.java
new file mode 100644
index 000000000..48235338e
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/reader/osm/RestrictionTagParserTest.java
@@ -0,0 +1,177 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static com.graphhopper.reader.osm.RestrictionType.NO;
+import static com.graphhopper.reader.osm.RestrictionType.ONLY;
+import static org.junit.jupiter.api.Assertions.*;
+
+class RestrictionTagParserTest {
+    private final Map<String, Object> tags = new LinkedHashMap<>();
+
+    @BeforeEach
+    void setup() {
+        tags.clear();
+        tags.put("type", "restriction");
+    }
+
+    private RestrictionTagParser.Result parseForVehicleTypes(String... vehicleTypes) throws OSMRestrictionException {
+        return new RestrictionTagParser(Arrays.asList(vehicleTypes), null).parseRestrictionTags(tags);
+    }
+
+    @Test
+    void noRestrictions() {
+        OSMRestrictionException e = assertThrows(OSMRestrictionException.class, () -> parseForVehicleTypes("motorcar"));
+        assertTrue(e.getMessage().contains("neither has a 'restriction' nor 'restriction:' tags"), e.getMessage());
+    }
+
+    @Test
+    void exceptButNoRestriction() {
+        tags.put("restriction:bicycle", "no_right_turn");
+        tags.put("except", "psv");
+        OSMRestrictionException e = assertThrows(OSMRestrictionException.class, () -> parseForVehicleTypes("psv"));
+        assertTrue(e.getMessage().contains("has an 'except', but no 'restriction' or 'restriction:conditional' tag"), e.getMessage());
+    }
+
+    @Test
+    void exceptButOnlyLimitedRestriction() {
+        tags.put("restriction:hgv:conditional", "no_right_turn @ (weight > 3.5)");
+        tags.put("except", "psv");
+        OSMRestrictionException e = assertThrows(OSMRestrictionException.class, () -> parseForVehicleTypes("psv"));
+        assertTrue(e.getMessage().contains("has an 'except', but no 'restriction' or 'restriction:conditional' tag"), e.getMessage());
+    }
+
+    @Test
+    void exceptWithConditional() throws OSMRestrictionException {
+        tags.put("restriction:conditional", "no_right_turn @ (weight > 3.5)");
+        tags.put("except", "psv");
+        // we do not handle conditional restrictions yet, but no warning for except+restriction:conditional,
+        // because this combination of tags could make sense
+        assertNull(parseForVehicleTypes("psv"));
+    }
+
+    @Test
+    void restrictionAndLimitedRestriction() {
+        // restriction and restriction:vehicle
+        tags.put("restriction", "no_left_turn");
+        tags.put("restriction:psv", "no_left_turn");
+        OSMRestrictionException e = assertThrows(OSMRestrictionException.class, () -> parseForVehicleTypes("psv"));
+        assertTrue(e.getMessage().contains("has a 'restriction' tag, but also 'restriction:' tags"), e.getMessage());
+    }
+
+    @Test
+    void restrictionAndLimitedRestriction_giveWay() throws OSMRestrictionException {
+        // this could happen for real, or at least it wouldn't be nonsensical. we ignore give_way so far, but do not
+        // ignore or warn about the entire relation
+        tags.put("restriction:bicycle", "give_way");
+        tags.put("restriction", "no_left_turn");
+        RestrictionTagParser.Result res = parseForVehicleTypes("bicycle");
+        assertEquals("no_left_turn", res.getRestriction());
+        assertEquals(NO, res.getRestrictionType());
+    }
+
+    @Test
+    void bicycle_giveWay() {
+        tags.put("restriction:bicycle", "give_way");
+        OSMRestrictionException e = assertThrows(OSMRestrictionException.class, () -> parseForVehicleTypes("bicycle"));
+        assertTrue(e.isWithoutWarning());
+    }
+
+    @Test
+    void conditional() throws OSMRestrictionException {
+        // So far we are ignoring conditional restrictions, even though for example weight restrictions could
+        // be interesting, even though some of them could probably be tagged as restriction:hgv
+        tags.put("restriction:conditional", "no_left_turn @ (weight > 3.5)");
+        assertNull(parseForVehicleTypes("motorcar"));
+    }
+
+    @Test
+    void except() throws OSMRestrictionException {
+        tags.put("restriction", "no_left_turn");
+        tags.put("except", "psv");
+        assertNull(parseForVehicleTypes("psv"));
+    }
+
+    @Test
+    void exceptOther() throws OSMRestrictionException {
+        tags.put("restriction", "only_left_turn");
+        tags.put("except", "psv");
+        RestrictionTagParser.Result res = parseForVehicleTypes("motorcar");
+        assertEquals("only_left_turn", res.getRestriction());
+        assertEquals(ONLY, res.getRestrictionType());
+    }
+
+    @Test
+    void limitedToVehicle() throws OSMRestrictionException {
+        tags.put("restriction:motorcar", "no_left_turn");
+        RestrictionTagParser.Result res = parseForVehicleTypes("motorcar");
+        assertEquals("no_left_turn", res.getRestriction());
+        assertEquals(NO, res.getRestrictionType());
+    }
+
+    @Test
+    void limitedToOtherVehicle() throws OSMRestrictionException {
+        tags.put("restriction:motorcar", "no_left_turn");
+        RestrictionTagParser.Result res = parseForVehicleTypes("bicycle");
+        assertNull(res);
+    }
+
+    @Test
+    void limitedMultiple() throws OSMRestrictionException {
+        tags.put("restriction:motorcar", "no_left_turn");
+        tags.put("restriction:psv", "no_left_turn");
+        RestrictionTagParser.Result res = parseForVehicleTypes("motorcar");
+        assertEquals("no_left_turn", res.getRestriction());
+        assertEquals(NO, res.getRestrictionType());
+    }
+
+    @Test
+    void limitedMultipleOther() throws OSMRestrictionException {
+        tags.put("restriction:motorcar", "no_left_turn");
+        tags.put("restriction:psv", "no_left_turn");
+        RestrictionTagParser.Result res = parseForVehicleTypes("bicycle");
+        assertNull(res);
+    }
+
+    @Test
+    void multipleVehicleTypes() throws OSMRestrictionException {
+        tags.put("restriction", "only_right_turn");
+        RestrictionTagParser.Result res = parseForVehicleTypes("motorcar", "motor_vehicle", "vehicle");
+        assertEquals("only_right_turn", res.getRestriction());
+        assertEquals(ONLY, res.getRestrictionType());
+    }
+
+    @Test
+    void exceptBus() throws OSMRestrictionException {
+        tags.put("restriction", "only_right_turn");
+        // todo: how should we handle except=bus? should it be excluded for psv?
+        tags.put("except", "psv");
+        RestrictionTagParser.Result res = parseForVehicleTypes("motorcar", "motor_vehicle", "vehicle");
+        assertEquals("only_right_turn", res.getRestriction());
+        assertEquals(ONLY, res.getRestrictionType());
+        assertNull(parseForVehicleTypes("psv"));
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/reader/osm/WayToEdgeConverterTest.java b/core/src/test/java/com/graphhopper/reader/osm/WayToEdgeConverterTest.java
new file mode 100644
index 000000000..d6a2cdc69
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/reader/osm/WayToEdgeConverterTest.java
@@ -0,0 +1,66 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+import com.carrotsearch.hppc.IntArrayList;
+import com.carrotsearch.hppc.LongArrayList;
+import com.carrotsearch.hppc.cursors.IntCursor;
+import com.graphhopper.storage.BaseGraph;
+import org.junit.jupiter.api.Test;
+
+import java.util.Iterator;
+import java.util.function.LongFunction;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class WayToEdgeConverterTest {
+
+    @Test
+    void convertForViaWays() throws OSMRestrictionException {
+        BaseGraph graph = new BaseGraph.Builder(1).create();
+        for (int i = 0; i < 10; i++)
+            graph.edge(i, i + 1);
+        WayToEdgeConverter.EdgeResult edgeResult = new WayToEdgeConverter(graph, way -> IntArrayList.from(Math.toIntExact(way)).iterator())
+                .convertForViaWays(ways(0), ways(2, 6, 4, 1, 7, 3, 5, 8), ways(9));
+        assertEquals(IntArrayList.from(1, 2, 3, 4, 5, 6, 7, 8), edgeResult.getViaEdges());
+        assertEquals(IntArrayList.from(1, 2, 3, 4, 5, 6, 7, 8, 9), edgeResult.getNodes());
+    }
+
+    @Test
+    void convertForViaWays_throwsIfViaWayIsSplitIntoMultipleEdges() {
+        BaseGraph graph = new BaseGraph.Builder(1).create();
+        graph.edge(0, 1);
+        graph.edge(1, 2);
+        graph.edge(2, 3);
+        graph.edge(3, 4);
+        LongFunction<Iterator<IntCursor>> edgesByWay = way -> {
+            // way 0 and 2 simply correspond to edges 0 and 3, but way 1 is split into the two edges 1 and 2
+            if (way == 1) return IntArrayList.from(1, 2).iterator();
+            else return IntArrayList.from(Math.toIntExact(way)).iterator();
+        };
+        OSMRestrictionException e = assertThrows(OSMRestrictionException.class,
+                () -> new WayToEdgeConverter(graph, edgesByWay).convertForViaWays(ways(0), ways(1), ways(2)));
+        assertTrue(e.getMessage().contains("has via member way that isn't split at adjacent ways"), e.getMessage());
+    }
+
+    private LongArrayList ways(long... ways) {
+        return LongArrayList.from(ways);
+    }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/reader/osm/WayToEdgesMapTest.java b/core/src/test/java/com/graphhopper/reader/osm/WayToEdgesMapTest.java
new file mode 100644
index 000000000..e1505d9b3
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/reader/osm/WayToEdgesMapTest.java
@@ -0,0 +1,105 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.reader.osm;
+
+import com.carrotsearch.hppc.IntArrayList;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class WayToEdgesMapTest {
+
+    @Test
+    void notReserved() {
+        WayToEdgesMap wayToEdgesMap = new WayToEdgesMap();
+        wayToEdgesMap.reserve(5);
+        wayToEdgesMap.putIfReserved(5, 4);
+        wayToEdgesMap.putIfReserved(6, 2);
+        checkEdges(wayToEdgesMap, 5, 4);
+        // since we did not reserve 6, no edges were added. This is exactly what we want for turn restrictions when
+        // reading OSM: first we reserve all the ways that appear in restriction relations and later we only add the
+        // edges for these (but not other) ways.
+        checkEdges(wayToEdgesMap, 6);
+    }
+
+    @Test
+    void waysMustBeInOrder() {
+        WayToEdgesMap wayToEdgesMap = new WayToEdgesMap();
+        wayToEdgesMap.reserve(10);
+        wayToEdgesMap.reserve(11);
+        wayToEdgesMap.putIfReserved(10, 3);
+        wayToEdgesMap.putIfReserved(11, 5);
+        assertThrows(IllegalArgumentException.class, () -> wayToEdgesMap.putIfReserved(10, 3));
+    }
+
+    @Test
+    void basic() {
+        WayToEdgesMap wayToEdgesMap = new WayToEdgesMap();
+        wayToEdgesMap.reserve(10);
+        wayToEdgesMap.reserve(11);
+        wayToEdgesMap.reserve(12);
+        wayToEdgesMap.reserve(6);
+        wayToEdgesMap.reserve(1234);
+        wayToEdgesMap.reserve(7);
+        wayToEdgesMap.putIfReserved(10, 3);
+        wayToEdgesMap.putIfReserved(10, 5);
+        wayToEdgesMap.putIfReserved(6, 1);
+        wayToEdgesMap.putIfReserved(1234, 12);
+        wayToEdgesMap.putIfReserved(1234, 13);
+        wayToEdgesMap.putIfReserved(1234, 13);
+        wayToEdgesMap.putIfReserved(7, 2);
+        checkEdges(wayToEdgesMap, 10, 3, 5);
+        checkEdges(wayToEdgesMap, 6, 1);
+        checkEdges(wayToEdgesMap, 1234, 12, 13, 13);
+        checkEdges(wayToEdgesMap, 7, 2);
+        checkEdges(wayToEdgesMap, 42);
+    }
+
+    @Test
+    void another() {
+        WayToEdgesMap wayToEdgesMap = new WayToEdgesMap();
+        wayToEdgesMap.reserve(1);
+        wayToEdgesMap.reserve(2);
+        wayToEdgesMap.reserve(3);
+        wayToEdgesMap.putIfReserved(1, 0);
+        wayToEdgesMap.putIfReserved(1, 1);
+        wayToEdgesMap.putIfReserved(3, 3);
+        wayToEdgesMap.putIfReserved(3, 4);
+        checkEdges(wayToEdgesMap, 1, 0, 1);
+        checkEdges(wayToEdgesMap, 3, 3, 4);
+    }
+
+    @Test
+    void reserveButDoNotPut() {
+        WayToEdgesMap wayToEdgesMap = new WayToEdgesMap();
+        wayToEdgesMap.reserve(1);
+        wayToEdgesMap.reserve(2);
+        wayToEdgesMap.putIfReserved(2, 42);
+        checkEdges(wayToEdgesMap, 1);
+        checkEdges(wayToEdgesMap, 2, 42);
+    }
+
+    private void checkEdges(WayToEdgesMap wayToEdgesMap, long way, int... expected) {
+        IntArrayList edges = new IntArrayList();
+        wayToEdgesMap.getEdges(way).forEachRemaining(c -> edges.add(c.value));
+        assertEquals(IntArrayList.from(expected), edges);
+    }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMTurnRestrictionParserTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMTurnRestrictionParserTest.java
deleted file mode 100644
index d4585fc04..000000000
--- a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMTurnRestrictionParserTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.graphhopper.routing.util.parsers;
-
-import com.graphhopper.reader.osm.OSMTurnRestriction;
-import com.graphhopper.routing.ev.*;
-import com.graphhopper.routing.util.EncodingManager;
-import com.graphhopper.routing.util.TransportationMode;
-import com.graphhopper.storage.BaseGraph;
-import com.graphhopper.storage.TurnCostStorage;
-import com.graphhopper.util.GHUtility;
-import org.junit.jupiter.api.Test;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class OSMTurnRestrictionParserTest {
-
-    @Test
-    public void testGetRestrictionAsEntries() {
-        BooleanEncodedValue accessEnc = new SimpleBooleanEncodedValue("access", true);
-        DecimalEncodedValue speedEnc = new DecimalEncodedValueImpl("speed", 5, 5, false);
-        DecimalEncodedValue turnCostEnc = TurnCost.create("car", 1);
-        EncodingManager em = EncodingManager.start().add(accessEnc).add(speedEnc).addTurnCostEncodedValue(turnCostEnc).build();
-        final Map<Long, Integer> osmNodeToInternal = new HashMap<>();
-        final Map<Integer, Long> internalToOSMEdge = new HashMap<>();
-
-        osmNodeToInternal.put(3L, 3);
-        // edge ids are only stored if they occurred before in an OSMRelation
-        internalToOSMEdge.put(3, 3L);
-        internalToOSMEdge.put(4, 4L);
-
-        OSMTurnRestrictionParser parser = new OSMTurnRestrictionParser(accessEnc, turnCostEnc, OSMRoadAccessParser.toOSMRestrictions(TransportationMode.CAR));
-        BaseGraph graph = new BaseGraph.Builder(em.getIntsForFlags()).withTurnCosts(true).create();
-        initGraph(graph, accessEnc, speedEnc);
-        TurnCostParser.ExternalInternalMap map = new TurnCostParser.ExternalInternalMap() {
-
-            @Override
-            public int getInternalNodeIdOfOsmNode(long nodeOsmId) {
-                return osmNodeToInternal.getOrDefault(nodeOsmId, -1);
-            }
-
-            @Override
-            public long getOsmIdOfInternalEdge(int edgeId) {
-                Long l = internalToOSMEdge.get(edgeId);
-                if (l == null)
-                    return -1;
-                return l;
-            }
-        };
-
-        // TYPE == ONLY
-        OSMTurnRestriction instance = new OSMTurnRestriction(4, 3, 3, OSMTurnRestriction.RestrictionType.ONLY);
-        parser.addRelationToTCStorage(instance, map, graph);
-
-        TurnCostStorage tcs = graph.getTurnCostStorage();
-        assertTrue(Double.isInfinite(tcs.get(turnCostEnc, 4, 3, 6)));
-        assertEquals(0, tcs.get(turnCostEnc, 4, 3, 3), .1);
-        assertTrue(Double.isInfinite(tcs.get(turnCostEnc, 4, 3, 2)));
-
-        // TYPE == NOT
-        instance = new OSMTurnRestriction(4, 3, 3, OSMTurnRestriction.RestrictionType.NOT);
-        parser.addRelationToTCStorage(instance, map, graph);
-        assertTrue(Double.isInfinite(tcs.get(turnCostEnc, 4, 3, 3)));
-    }
-
-    // 0---1
-    // |   /
-    // 2--3--4
-    // |  |  |
-    // 5--6--7
-    private static void initGraph(BaseGraph graph, BooleanEncodedValue accessEnc, DecimalEncodedValue speedEnc) {
-        GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(0, 1).setDistance(3));
-        GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(0, 2).setDistance(1));
-        GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(1, 3).setDistance(1));
-        GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(2, 3).setDistance(1));
-        GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(3, 4).setDistance(1));
-        GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(2, 5).setDistance(0.5));
-        GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(3, 6).setDistance(1));
-        GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(4, 7).setDistance(1));
-        GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(5, 6).setDistance(1));
-        GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(6, 7).setDistance(1));
-    }
-}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/RestrictionSetterTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/RestrictionSetterTest.java
new file mode 100644
index 000000000..10c17c6b5
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/RestrictionSetterTest.java
@@ -0,0 +1,294 @@
+package com.graphhopper.routing.util.parsers;
+
+import com.carrotsearch.hppc.IntArrayList;
+import com.graphhopper.reader.osm.GraphRestriction;
+import com.graphhopper.reader.osm.Pair;
+import com.graphhopper.reader.osm.RestrictionType;
+import com.graphhopper.routing.Dijkstra;
+import com.graphhopper.routing.ev.DecimalEncodedValue;
+import com.graphhopper.routing.ev.EncodedValue;
+import com.graphhopper.routing.ev.TurnCost;
+import com.graphhopper.routing.util.TraversalMode;
+import com.graphhopper.routing.weighting.DefaultTurnCostProvider;
+import com.graphhopper.routing.weighting.TurnCostProvider;
+import com.graphhopper.routing.weighting.Weighting;
+import com.graphhopper.storage.BaseGraph;
+import com.graphhopper.util.EdgeIteratorState;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class RestrictionSetterTest {
+    private static final IntArrayList NO_PATH = IntArrayList.from();
+    private BaseGraph graph;
+    private RestrictionSetter r;
+
+    @BeforeEach
+    void setup() {
+        graph = new BaseGraph.Builder(1).withTurnCosts(true).create();
+        r = new RestrictionSetter(graph);
+    }
+
+    @Test
+    void viaNode_no() {
+        // 0-1-2
+        //   | |
+        //   3-4
+        int a = edge(0, 1);
+        int b = edge(1, 2);
+        edge(1, 3);
+        edge(2, 4);
+        edge(3, 4);
+        GraphRestriction graphRestriction = GraphRestriction.node(a, 1, b);
+        DecimalEncodedValue turnCostEnc = createTurnCostEnc("car");
+        r.setRestrictions(Arrays.asList(new Pair<>(graphRestriction, RestrictionType.NO)), turnCostEnc);
+        assertEquals(nodes(0, 1, 3, 4, 2), calcPath(0, 2, turnCostEnc));
+    }
+
+    @Test
+    void viaNode_only() {
+        // 0-1-2
+        //   | |
+        //   3-4
+        int a = edge(0, 1);
+        int b = edge(1, 2);
+        edge(1, 3);
+        edge(2, 4);
+        edge(3, 4);
+        GraphRestriction graphRestriction = GraphRestriction.node(a, 1, b);
+        DecimalEncodedValue turnCostEnc = createTurnCostEnc("car");
+        r.setRestrictions(Arrays.asList(new Pair<>(graphRestriction, RestrictionType.ONLY)), turnCostEnc);
+        assertEquals(nodes(0, 1, 2, 4, 3), calcPath(0, 3, turnCostEnc));
+    }
+
+    @Test
+    void viaWay_no() {
+        //     4
+        //  a b|c
+        // 0-1-2-3
+        //   | |
+        //   5 6
+        //   | |
+        //   8-9
+        int a = edge(0, 1);
+        int b = edge(1, 2);
+        int c = edge(2, 3);
+        edge(2, 4);
+        edge(1, 5);
+        edge(5, 8);
+        edge(2, 6);
+        edge(6, 9);
+        edge(8, 9);
+        GraphRestriction graphRestriction = GraphRestriction.way(a, b, c, nodes(1, 2));
+        DecimalEncodedValue turnCostEnc = createTurnCostEnc("car");
+        r.setRestrictions(Arrays.asList(
+                new Pair<>(graphRestriction, RestrictionType.NO)
+        ), turnCostEnc);
+        // turning from a to b and then to c is not allowed
+        assertEquals(nodes(0, 1, 5, 8, 9, 6, 2, 3), calcPath(0, 3, turnCostEnc));
+        // turning from a to b, or b to c is still allowed
+        assertEquals(nodes(0, 1, 2, 4), calcPath(0, 4, turnCostEnc));
+        assertEquals(nodes(5, 1, 2, 3), calcPath(5, 3, turnCostEnc));
+    }
+
+    @Test
+    void viaWay_no_withOverlap() {
+        //   a   b   c   d
+        // 0---1---2---3---4
+        //     |s  |t  |u
+        //     5   6   7
+        int a = edge(0, 1);
+        int b = edge(1, 2);
+        int c = edge(2, 3);
+        int d = edge(3, 4);
+        int s = edge(1, 5);
+        int t = edge(2, 6);
+        int u = edge(3, 7);
+
+        DecimalEncodedValue turnCostEnc = createTurnCostEnc("car");
+        r.setRestrictions(Arrays.asList(
+                new Pair<>(GraphRestriction.way(a, b, c, nodes(1, 2)), RestrictionType.NO),
+                new Pair<>(GraphRestriction.way(b, c, d, nodes(2, 3)), RestrictionType.NO)
+        ), turnCostEnc);
+
+        assertEquals(NO_PATH, calcPath(0, 3, turnCostEnc)); // a-b-c
+        assertEquals(nodes(0, 1, 2, 6), calcPath(0, 6, turnCostEnc)); // a-b-t
+        assertEquals(nodes(5, 1, 2, 3), calcPath(5, 3, turnCostEnc)); // s-b-c
+        assertEquals(nodes(5, 1, 2, 6), calcPath(5, 6, turnCostEnc)); // s-b-t
+
+        assertEquals(NO_PATH, calcPath(1, 4, turnCostEnc)); // b-c-d
+        assertEquals(nodes(1, 2, 3, 7), calcPath(1, 7, turnCostEnc)); // b-c-u
+        assertEquals(nodes(6, 2, 3, 4), calcPath(6, 4, turnCostEnc)); // t-c-d
+        assertEquals(nodes(6, 2, 3, 7), calcPath(6, 7, turnCostEnc)); // t-c-u
+    }
+
+    @Test
+    void viaWay_no_withOverlap_more_complex() {
+        //    0   1
+        //    | a |
+        // 2--3---4--5
+        //   b|   |d
+        // 6--7---8--9
+        //    | c |
+        //   10---11
+        int s = edge(0, 3);
+        edge(1, 4);
+        edge(2, 3);
+        int a = edge(3, 4);
+        int t = edge(4, 5);
+        int b = edge(3, 7);
+        int d = edge(4, 8);
+        edge(6, 7);
+        int c = edge(7, 8);
+        edge(8, 9);
+        edge(7, 10);
+        edge(8, 11);
+        edge(10, 11);
+        DecimalEncodedValue turnCostEnc = createTurnCostEnc("car");
+        r.setRestrictions(Arrays.asList(
+                new Pair<>(GraphRestriction.node(t, 4, d), RestrictionType.NO),
+                new Pair<>(GraphRestriction.node(s, 3, a), RestrictionType.NO),
+                new Pair<>(GraphRestriction.way(a, b, c, nodes(3, 7)), RestrictionType.NO),
+                new Pair<>(GraphRestriction.way(b, c, d, nodes(7, 8)), RestrictionType.NO),
+                new Pair<>(GraphRestriction.way(c, d, a, nodes(8, 4)), RestrictionType.NO),
+                new Pair<>(GraphRestriction.way(d, a, b, nodes(4, 3)), RestrictionType.NO)
+        ), turnCostEnc);
+
+        assertEquals(nodes(0, 3, 7, 8, 9), calcPath(0, 9, turnCostEnc));
+        assertEquals(nodes(5, 4, 3, 7, 10, 11, 8, 9), calcPath(5, 9, turnCostEnc));
+        assertEquals(nodes(5, 4, 3, 2), calcPath(5, 2, turnCostEnc));
+        assertEquals(nodes(0, 3, 7, 10), calcPath(0, 10, turnCostEnc));
+        assertEquals(nodes(6, 7, 8, 9), calcPath(6, 9, turnCostEnc));
+    }
+
+    @Test
+    void viaWay_only() {
+        //      0
+        //  a   |b  c
+        // 1----2----3
+        //      |d
+        // 4----5----6
+        //  e   |f  g
+        //      7
+        int a = edge(1, 2);
+        int b = edge(0, 2);
+        int c = edge(2, 3);
+        int d = edge(2, 5);
+        int e = edge(4, 5);
+        int f = edge(5, 7);
+        int g = edge(5, 6);
+        DecimalEncodedValue turnCostEnc = createTurnCostEnc("car");
+        r.setRestrictions(Arrays.asList(
+                new Pair<>(GraphRestriction.way(a, d, f, nodes(2, 5)), RestrictionType.ONLY),
+                // we add a few more restrictions, because that happens a lot in real data
+                new Pair<>(GraphRestriction.way(c, d, g, nodes(2, 5)), RestrictionType.NO),
+                new Pair<>(GraphRestriction.node(e, 5, f), RestrictionType.NO)
+        ), turnCostEnc);
+        // following the restriction is allowed of course
+        assertEquals(nodes(1, 2, 5, 7), calcPath(1, 7, turnCostEnc));
+        // taking another turn at the beginning is not allowed
+        assertEquals(nodes(), calcPath(1, 3, turnCostEnc));
+        // taking another turn after the first turn is not allowed either
+        assertEquals(nodes(), calcPath(1, 4, turnCostEnc));
+        // coming from somewhere we can go anywhere
+        assertEquals(nodes(0, 2, 5, 6), calcPath(0, 6, turnCostEnc));
+        assertEquals(nodes(0, 2, 5, 7), calcPath(0, 7, turnCostEnc));
+    }
+
+    @Test
+    void viaWay_only_twoRestrictionsSharingSameVia() {
+        //   a   c   d
+        // 0---1---2---3
+        //     |b  |e
+        // 5--/     \--4
+        int a = edge(0, 1);
+        int b = edge(5, 1);
+        int c = edge(1, 2);
+        int d = edge(2, 3);
+        int e = edge(2, 4);
+        DecimalEncodedValue turnCostEnc = createTurnCostEnc("car");
+        assertThrows(IllegalStateException.class, () -> r.setRestrictions(Arrays.asList(
+                        // These are two 'only' via-way restrictions that share the same via way. A real-world example can
+                        // be found in Rüdesheim am Rhein where vehicles either have to go straight or enter the ferry depending
+                        // on the from-way, even though they use the same via way before.
+                        // We have to make sure such cases are ignored already when we parse the OSM data.
+                        new Pair<>(GraphRestriction.way(a, c, d, nodes(1, 2)), RestrictionType.ONLY),
+                        new Pair<>(GraphRestriction.way(b, c, e, nodes(1, 2)), RestrictionType.ONLY)
+                ), turnCostEnc)
+        );
+    }
+
+    private static DecimalEncodedValue createTurnCostEnc(String name) {
+        DecimalEncodedValue turnCostEnc = TurnCost.create(name, 1);
+        turnCostEnc.init(new EncodedValue.InitializerConfig());
+        return turnCostEnc;
+    }
+
+    private IntArrayList calcPath(int from, int to, DecimalEncodedValue turnCostEnc) {
+        return new IntArrayList(new Dijkstra(graph, new MyWeighting(graph, turnCostEnc), TraversalMode.EDGE_BASED).calcPath(from, to).calcNodes());
+    }
+
+    private IntArrayList nodes(int... nodes) {
+        return IntArrayList.from(nodes);
+    }
+
+    private IntArrayList edges(int... edges) {
+        return IntArrayList.from(edges);
+    }
+
+    private int edge(int from, int to) {
+        return graph.edge(from, to).setDistance(10).getEdge();
+    }
+
+    private static class MyWeighting implements Weighting {
+        private final TurnCostProvider turnCostProvider;
+
+        public MyWeighting(BaseGraph graph, DecimalEncodedValue turnCostEnc) {
+            turnCostProvider = new DefaultTurnCostProvider(turnCostEnc, graph.getTurnCostStorage());
+        }
+
+        @Override
+        public double getMinWeight(double distance) {
+            return 0;
+        }
+
+        @Override
+        public boolean edgeHasNoAccess(EdgeIteratorState edgeState, boolean reverse) {
+            return false;
+        }
+
+        @Override
+        public double calcEdgeWeight(EdgeIteratorState edgeState, boolean reverse) {
+            return edgeState.getDistance();
+        }
+
+        @Override
+        public long calcEdgeMillis(EdgeIteratorState edgeState, boolean reverse) {
+            return 0;
+        }
+
+        @Override
+        public double calcTurnWeight(int inEdge, int viaNode, int outEdge) {
+            return turnCostProvider.calcTurnWeight(inEdge, viaNode, outEdge);
+        }
+
+        @Override
+        public long calcTurnMillis(int inEdge, int viaNode, int outEdge) {
+            return 0;
+        }
+
+        @Override
+        public boolean hasTurnCosts() {
+            return true;
+        }
+
+        @Override
+        public String getName() {
+            return "test";
+        }
+    }
+}
\ No newline at end of file