Repository: streetcomplete/StreetComplete (language: kotlin)

Issue:
Crash when adding a parking lane
The crash only happens with a specific way, but is deterministic.

**How to Reproduce**
* Select this way in the parking lane overlay: https://www.openstreetmap.org/way/4014978
* Then select the south side and add “parallel on street side” (screenshot below). Leave the north side as-is.
* The app crashes when confirming.

**Expected Behavior**
I would expect the app to add the information I gave to the map.

Alternatively, if the way was not meant to be added a parking lane, then I would not expect the app to let me select it in the first place.

**Additional information**
Screenshot:
![Screenshot_2022-11-15-10-45-48-614_de westnordost streetcomplete](https://user-images.githubusercontent.com/1829786/201890408-1a79fd86-3e00-4625-a645-72ffd644d555.jpg)

Here is the backtrace that I get:
```
java.lang.IllegalArgumentException: Attempting to tag invalid parking lane
	at de.westnordost.streetcomplete.osm.street_parking.StreetParkingKt.toOsmLaneValue(StreetParking.kt:122)
	at de.westnordost.streetcomplete.osm.street_parking.StreetParkingKt.applyTo(StreetParking.kt:92)
	at de.westnordost.streetcomplete.overlays.street_parking.StreetParkingOverlayForm.onClickOk(StreetParkingOverlayForm.kt:136)
	at de.westnordost.streetcomplete.overlays.AbstractOverlayForm.onViewCreated$lambda-6(AbstractOverlayForm.kt:194)
	at de.westnordost.streetcomplete.overlays.AbstractOverlayForm.$r8$lambda$18Xv-GzCmBt76jzZFt341LP4EVc(AbstractOverlayForm.kt)
	at de.westnordost.streetcomplete.overlays.AbstractOverlayForm$$ExternalSyntheticLambda2.onClick(R8$$SyntheticClass)
	at android.view.View.performClick(View.java:5647)
	at android.view.View$PerformClick.run(View.java:22465)
	at android.os.Handler.handleCallback(Handler.java:754)
	at android.os.Handler.dispatchMessage(Handler.java:95)
	at android.os.Looper.loop(Looper.java:163)
	at android.app.ActivityThread.main(ActivityThread.java:6238)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:933)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:823)
```

Here are the keys of the way in question:
```
highway=service
maxspeed=30
name=Rue Eugène Chavant
parking:lane:both=parallel
surface=asphalt
```

The reason why I leave the north side as-is is that there is a discrepancy between what is meant to be and what there is: according to what the signs says, it should be `parking:lane:left=no`, but in practice people will informally park there where they can, so it’s more like `parking:lane:left=parallel`, `parking:lane:left:parallel=half_on_kerb`, leaving no actual space for pedestrians.
If the goal of this key is to measure the space dedicated to cars, I guess that the second would be more useful (as this part of the street is filled with cars), but if the goal is to tell drivers where to safely park, the first version is preferable. If we add to this the fact that the town actually uses OSM maps, then drivers could start complaining against fines because the town maps says so.
So really, there is no solution that I’m comfortable with about the north side of the street. The south is clear though, hence my answer ☺

**Versions affected**
Android version: 7.1.2 N2G47H
Kernel version: 3.18.31-perf-g6507e5c
StreetComplete version: v49.0



Discussion hints:

Pull request: Cycleway overlay
- refactored cycleway, street parking and sidewalk parsers to be be consistent and properly handle all edge cases when tagging only one side (fixes #4634). A side-effect of this is that the app will always merge same `*:left` and `*:right` values to `*:both` as was requested several times somewhere in this issue tracker and where I was like 🤷 before.
- refactored cycleway quest to share as much code with the overlay as possible
- fixed a bug in cycleway parsing: If in flow direction of a oneway there was a dual track or dual lane and in contra-flow direction there was nothing, the parser would wrongly interpret the contra-flow side as being "none, no oneway", i.e. as if bicycles could go in contraflow on the street itself
- made cycleway parser more strict: The parser does not accept values like `on_street`, `none` etc. anymore / classifies them as `INVALID` rather than `UNKNOWN`
- disabled cycleway quest
- added `cycleway=shoulder` option for overlay and quest (see [discussion](https://community.openstreetmap.org/t/rare-cycleway-value-shoulder-obsolete-or-useful/5849))
- sidewalk overlay: do not show missing sidewalks on roads with a speed limit <= 10 km/h and unpaved roads as missing
- sidewalk overlay: show sidewalks mapped on cycleways, paths etc
- sidewalk overlay: allow editing of sidewalks mapped on exclusive cycleways
- sidewalk overlay: prefer tagging `sidewalk:both=separate` over `sidewalk=separate`

### Features of the overlay

<img src="https://user-images.githubusercontent.com/4661658/204666419-e7a8eaf2-0423-45ff-969e-54688a9e0f1a.png" width="300"/>

Display and change cycleway layout.
- Cycleways behind the curb are blue-ish (a track, a shared sidewalk)
- Lanes on the street are yellow-ish
- Black is none
- Red is missing, invalid or ambiguos.

<img src="https://user-images.githubusercontent.com/4661658/204665334-7ff3bf12-9374-4027-96fc-bb8762bbde67.png" width="300"/>

Display bicycle boulevards, can be selected instead of defining cycleways left and right in countries where they exist legally, see [hasBicycleBoulevard.yml](https://github.com/streetcomplete/countrymetadata/blob/master/data/hasBicycleBoulevard.yml)


<img src="https://user-images.githubusercontent.com/4661658/204665468-0e0a0d91-5cd0-4021-88df-c1317f232b10.png" width="300"/>

Change the cycleway situation on separately mapped ways and paths.


https://user-images.githubusercontent.com/4661658/204665892-cdda5758-21c4-47d3-88d8-469f2088b218.mp4

Display and select to reverse the direction of cycleways (functionality also added to respective quest). 

This was necessary because the quest could ignore (=not show) roads tagged with such cycleways, while the overlay must support it in order to not overwrite the data with wrong data.

Yes, this very rarely actually happens, see
https://www.openstreetmap.org/way/1032480826
https://www.google.de/maps/@40.6761578,-73.9508024,3a,75y,266.58h,74.83t/data=!3m6!1e1!3m4!1snlCNLeAEszlMGEmNGcfURA!2e0!7i16384!8i8192

This required a large refactor, because the parser that reads the OSM data, the creator that puts the data model back into the tags and all of the display code needs to handle pairs of cycleway type + cycleway direction. Before, it just had to handle the cycleway type.

Test command: ./gradlew :app:testDebugUnitTest --tests "de.westnordost.streetcomplete.osm.cycleway_separate.SeparateCyclewayCreatorKtTest" --tests "de.westnordost.streetcomplete.osm.street_parking.StreetParkingKtTest" --tests "de.westnordost.streetcomplete.osm.cycleway.CyclewayCreatorKtTest" --tests "de.westnordost.streetcomplete.osm.cycleway.CyclewayParserKtTest" --tests "de.westnordost.streetcomplete.quests.sidewalk.AddSidewalkTest" --tests "de.westnordost.streetcomplete.osm.sidewalk.SidewalkCreatorKtTest" --tests "de.westnordost.streetcomplete.osm.cycleway_separate.SeparateCyclewayParserKtTest" --tests "de.westnordost.streetcomplete.quests.cycleway.AddCyclewayTest" --tests "de.westnordost.streetcomplete.osm.bicycle_boulevard.BicycleBoulevardKtTest" --tests "de.westnordost.streetcomplete.osm.cycleway.CyclewayKtTest" --tests "de.westnordost.streetcomplete.osm.OnewayKtTest" --tests "de.westnordost.streetcomplete.osm.street_parking.StreetParkingCreatorKtTest" --tests "de.westnordost.streetcomplete.osm.sidewalk.SidewalkKtTest"

Fix patch:
diff --git a/app/src/main/assets/country_metadata/AQ.yml b/app/src/main/assets/country_metadata/AQ.yml
new file mode 100644
index 00000000000..cc4a63ba104
--- /dev/null
+++ b/app/src/main/assets/country_metadata/AQ.yml
@@ -0,0 +1,2 @@
+# Do not edit. Data is from res/country/metadata and https://github.com/streetcomplete/countrymetadata
+noParkingLineStyle: yellow
diff --git a/app/src/main/assets/country_metadata/AT.yml b/app/src/main/assets/country_metadata/AT.yml
index 94acdecf713..37fd19700b1 100644
--- a/app/src/main/assets/country_metadata/AT.yml
+++ b/app/src/main/assets/country_metadata/AT.yml
@@ -6,6 +6,7 @@ clothesContainerOperators: [Humana, Holding Graz, Kolping, Rotes Kreuz, MA 48, C
 exclusiveCycleLaneStyle: white
 hasAdvisoryCycleLane: true
 hasAdvisorySpeedLimitSign: false
+hasBicycleBoulevard: true
 hasDailyAlternateSideParkingSign: true
 hasLivingStreet: true
 hasSlowZone: true
diff --git a/app/src/main/assets/country_metadata/BE.yml b/app/src/main/assets/country_metadata/BE.yml
index 39d4af56504..8cebdcd0cbb 100644
--- a/app/src/main/assets/country_metadata/BE.yml
+++ b/app/src/main/assets/country_metadata/BE.yml
@@ -7,6 +7,7 @@ exclusiveCycleLaneStyle: white dashes on both sides
 hasAdvisoryCycleLane: true
 hasAdvisorySpeedLimitSign: false
 hasBiWeeklyAlternateSideParkingSign: true
+hasBicycleBoulevard: true
 hasLivingStreet: true
 hasSlowZone: true
 livingStreetSignStyle: vienna
diff --git a/app/src/main/assets/country_metadata/CH.yml b/app/src/main/assets/country_metadata/CH.yml
index 8a8952dde24..ba5fdc05840 100644
--- a/app/src/main/assets/country_metadata/CH.yml
+++ b/app/src/main/assets/country_metadata/CH.yml
@@ -5,6 +5,7 @@ clothesContainerOperators: [Texaid, Ville de Geneve, Contex, coop, TexAid, Coop,
 exclusiveCycleLaneStyle: yellow dashes
 hasAdvisoryCycleLane: false
 hasAdvisorySpeedLimitSign: false
+hasBicycleBoulevard: true
 hasLivingStreet: true
 hasSlowZone: true
 livingStreetSignStyle: vienna
diff --git a/app/src/main/assets/country_metadata/DE.yml b/app/src/main/assets/country_metadata/DE.yml
index 78bc5e39e34..e689c5f10ca 100644
--- a/app/src/main/assets/country_metadata/DE.yml
+++ b/app/src/main/assets/country_metadata/DE.yml
@@ -7,6 +7,7 @@ edgeLineStyle: white
 exclusiveCycleLaneStyle: white
 hasAdvisoryCycleLane: true
 hasAdvisorySpeedLimitSign: false
+hasBicycleBoulevard: true
 hasLivingStreet: true
 hasSlowZone: true
 livingStreetSignStyle: vienna
diff --git a/app/src/main/assets/country_metadata/DK.yml b/app/src/main/assets/country_metadata/DK.yml
index b6572f47778..10635cc065d 100644
--- a/app/src/main/assets/country_metadata/DK.yml
+++ b/app/src/main/assets/country_metadata/DK.yml
@@ -1,7 +1,10 @@
 # Do not edit. Data is from res/country/metadata and https://github.com/streetcomplete/countrymetadata
+advisoryCycleLaneStyle: white dashes without pictogram
 atmOperators: [Danske Bank, Nordea, Euronet, Sydbank, Jyske Bank, Arbejdernes Landsbank, Spar Nord, Nykredit, Sparekassen Kronjylland, Nordjyske Bank, Andelskassen, Sparekassen Sjælland, Totalbanken, Lån & Spar, Handelsbanken, SparekassenThy, SparNord, Vestjysk Bank, Skjern Bank, DiBa, Sparekassen Vendsyssel, Jutlander Bank, Sparekassen Thy, Ringkøbing Landbobank, Frøs Herreds Sparekasse, Lån & Spar Bank, Bank Nordik, Djurslands Bank]
 exclusiveCycleLaneStyle: white
+hasAdvisoryCycleLane: true
 hasAdvisorySpeedLimitSign: true
+hasBicycleBoulevard: true
 hasLivingStreet: true
 hasSlowZone: true
 livingStreetSignStyle: vienna
diff --git a/app/src/main/assets/country_metadata/ES.yml b/app/src/main/assets/country_metadata/ES.yml
index b20512a1ce6..fbd1dd98684 100644
--- a/app/src/main/assets/country_metadata/ES.yml
+++ b/app/src/main/assets/country_metadata/ES.yml
@@ -8,6 +8,7 @@ exclusiveCycleLaneStyle: white
 hasAdvisoryCycleLane: false
 hasAdvisorySpeedLimitSign: true
 hasBiWeeklyAlternateSideParkingSign: true
+hasBicycleBoulevard: true
 hasDailyAlternateSideParkingSign: true
 hasLivingStreet: true
 hasSlowZone: true
diff --git a/app/src/main/assets/country_metadata/FI.yml b/app/src/main/assets/country_metadata/FI.yml
index 419dfa9b1ab..400950af0e7 100644
--- a/app/src/main/assets/country_metadata/FI.yml
+++ b/app/src/main/assets/country_metadata/FI.yml
@@ -5,6 +5,7 @@ clothesContainerOperators: [UFF, Rinki, HSY, Rosk'n roll, Rinki Oy, Fida, Suomen
 exclusiveCycleLaneStyle: white dashes
 hasAdvisoryCycleLane: false
 hasAdvisorySpeedLimitSign: true
+hasBicycleBoulevard: true
 hasDailyAlternateSideParkingSign: true
 hasLivingStreet: true
 hasSlowZone: true
diff --git a/app/src/main/assets/country_metadata/FR.yml b/app/src/main/assets/country_metadata/FR.yml
index 85b8a3d0771..91939c4d749 100644
--- a/app/src/main/assets/country_metadata/FR.yml
+++ b/app/src/main/assets/country_metadata/FR.yml
@@ -7,6 +7,7 @@ exclusiveCycleLaneStyle: white dashes
 hasAdvisoryCycleLane: true
 hasAdvisorySpeedLimitSign: true
 hasBiWeeklyAlternateSideParkingSign: true
+hasBicycleBoulevard: true
 hasLivingStreet: true
 hasSlowZone: true
 livingStreetSignStyle: france
diff --git a/app/src/main/assets/country_metadata/LI.yml b/app/src/main/assets/country_metadata/LI.yml
index baec77109f6..9a51d19ad36 100644
--- a/app/src/main/assets/country_metadata/LI.yml
+++ b/app/src/main/assets/country_metadata/LI.yml
@@ -2,6 +2,7 @@
 atmOperators: [Liechtensteinische Landesbank AG, Liechtensteinische Landesbank]
 exclusiveCycleLaneStyle: yellow
 hasAdvisoryCycleLane: false
+hasBicycleBoulevard: true
 hasSlowZone: true
 mobileCountryCode: 295
 noParkingLineStyle: dashed yellow line with Xs
diff --git a/app/src/main/assets/country_metadata/LU.yml b/app/src/main/assets/country_metadata/LU.yml
index 13b4c855db1..3ce79e2286d 100644
--- a/app/src/main/assets/country_metadata/LU.yml
+++ b/app/src/main/assets/country_metadata/LU.yml
@@ -1,10 +1,12 @@
 # Do not edit. Data is from res/country/metadata and https://github.com/streetcomplete/countrymetadata
-advisoryCycleLaneStyle: white dashes
+advisoryCycleLaneStyle: white dashes without pictogram
 atmOperators: [BCEE, BIL, P&T, BGL BNP Paribas, Raiffeisen, Banque Raiffeisen, Post, S-Bank, Banque Internationale à Luxembourg, Post.lu, BNP Paribas]
 chargingStationOperators: [Chargy, Enovos]
 exclusiveCycleLaneStyle: white
+hasAdvisoryCycleLane: true
 hasAdvisorySpeedLimitSign: true
 hasBiWeeklyAlternateSideParkingSign: true
+hasBicycleBoulevard: true
 hasLivingStreet: true
 hasSlowZone: true
 livingStreetSignStyle: vienna
diff --git a/app/src/main/assets/country_metadata/MC.yml b/app/src/main/assets/country_metadata/MC.yml
index 46e54ff537c..36e48b3e32b 100644
--- a/app/src/main/assets/country_metadata/MC.yml
+++ b/app/src/main/assets/country_metadata/MC.yml
@@ -2,6 +2,7 @@
 advisoryCycleLaneStyle: white dashes without pictogram
 exclusiveCycleLaneStyle: white dashes
 hasAdvisoryCycleLane: true
+hasBicycleBoulevard: true
 hasSlowZone: true
 mobileCountryCode: 212
 noParkingLineStyle: dashed yellow
diff --git a/app/src/main/assets/country_metadata/NL.yml b/app/src/main/assets/country_metadata/NL.yml
index 6cb965983cb..5f6b53f0bdb 100644
--- a/app/src/main/assets/country_metadata/NL.yml
+++ b/app/src/main/assets/country_metadata/NL.yml
@@ -5,6 +5,7 @@ chargingStationOperators: [Allego, NUON, Fastned, Nuon, EVBOX, Ecotap, Vattenfal
 clothesContainerOperators: [ROVA, Sympany, Leger des Heils ReShare, Leger des Heils, Gemeente Amsterdam, RD4, Curitas, Twiddus, Noppes, Leger des heils, hetgoed, Waardlanden, Opnieuw & Co, HVC]
 exclusiveCycleLaneStyle: white dashes
 hasAdvisoryCycleLane: true
+hasBicycleBoulevard: true
 hasLivingStreet: true
 hasSlowZone: true
 livingStreetSignStyle: vienna
diff --git a/app/src/main/assets/country_metadata/SV.yml b/app/src/main/assets/country_metadata/SV.yml
index fdf916d981d..69de988f1c7 100644
--- a/app/src/main/assets/country_metadata/SV.yml
+++ b/app/src/main/assets/country_metadata/SV.yml
@@ -1,8 +1,10 @@
 # Do not edit. Data is from res/country/metadata and https://github.com/streetcomplete/countrymetadata
+advisoryCycleLaneStyle: white dashes without pictogram
 atmOperators: [Banco Agrícola, Banco de América Central, Banco Cuscatlan, Scotiabank, Caja de Crédito, Davivienda]
 centerLineStyle: yellow
 exclusiveCycleLaneStyle: white dots
-hasAdvisoryCycleLane: false
+hasAdvisoryCycleLane: true
+hasBicycleBoulevard: true
 hasDailyAlternateSideParkingSign: true
 mobileCountryCode: 706
 noParkingSignStyle: mutcd latin
diff --git a/app/src/main/assets/country_metadata/US.yml b/app/src/main/assets/country_metadata/US.yml
index 43b2ba6bdfd..07245d4de7c 100644
--- a/app/src/main/assets/country_metadata/US.yml
+++ b/app/src/main/assets/country_metadata/US.yml
@@ -3,6 +3,7 @@ advisorySpeedLimitSignStyle: yellow
 atmOperators: [Bank of America, Wells Fargo, Chase, PNC Bank, US Bank, U.S. Bank, Citibank, Fifth Third Bank, KeyBank, BB&T, Banco Popular, TD Bank, BECU, Citizens Bank, Publix, Athena Bitcoin Inc., M&T Bank, Chase Bank, First Bank, SunTrust, PNC, MSUFCU, Commerce Bank, First National Bank, Allpoint, Cash Points, Bank of the West, Regions Bank, Cardtronics, Black Frog, BMO Harris Bank, Capital One, Huntington Bank, Union Bank, Wells Fargo Bank, Wells Fargo ATM, Santander, Huntington, State Employees' Credit Union, Partners Credit Union, First Hawaiian Bank, iTHINK Financial, First Citizens Bank, Navy Federal Credit Union, Pinnacle Bank, Regions, USAA, TCF Bank, MetaBank, ATM, Bitcoin of America, Rockitcoin, CEFCU, BBVA Compass, ANZ Guam, United Community Bank, Bank of Hawaii, Umpqua Bank, Columbia Bank, Cardtronics ATM, Associated Bank, Synovus, California Coast Credit Union, Arvest Bank, Great Western Bank, State Employee's Credit Union, San Diego County Credit Union, Access to Money, CU Anytime, Key Bank, SunTrust Bank, Local Government Federal Credit Union, OnPoint, Bank of Washington, CashPoints® ATM, LibertyX Bitcoin ATM, Wachovia, Swipe, Travelex, UW Credit Union, Cash Points ATM, 7-Eleven, Banner Bank, State Employees Credit Union, State Employee's Credit Union (ATM), Coin Cloud, Scotiabank, Kansas State Bank, ESL, Pioneer Bank, Alaska USA Federal Credit Union, Chase ATM, Comerica Bank, First National Bank of Omaha, Triton, AmeriCU Credit Union, Webster Bank, First Republic Bank, Great Southern Bank, Academy Bank, Capitol One, Founders Federal Credit Union, Heritage Bank, HomeStreet Bank, Athena Bitcoin ATM, Palmetto Citizens Federal Credit Union ATM, WesBanco, Valley Bank ATM, Venco, Michigan State Federal Credit Union, Eastern Bank, MSU Federal Credit Union, First Midwest Bank, California Bank & Trust, South Carolina Federal Credit Union, IBC Bank, First Financial Bank, Centennial Bank, Premier Bank, Washington Federal, Liberty Bank, Busey Bank, First State Bank, UMB Bank, Nautilus Hyosung, CapitolOne, Diebold Nixdorf, Bank of Springfield, Community First Credit Union, AlaskaUSA Federal Credit Union, Vermont Federal Credit Union, Randolph Brooks Federal Credit Union, Hancock Whitney, Citizen's Bank, Bay Federal Credit Union, USE Credit Union, AlaskaUSA, Associated Credit Union, Sovereign Bank, PenFed Credit Union, TCF, Golden 1 Credit Union, Michigan State University Federal Credit Union, Mission Federal Credit Union, M&T, First School, City National Bank, ESL Federal Credit Union, Florence Savings Bank, Comerica, Think Bank, Boeing Employees Credit Union, San Mateo Credit Union, Zions Bank, State Employees Credit Union (ATM), Walmart, Provident Credit Union, Charter One, Arvest, First Bank & Trust, Dacotah Bank, Suncoast Credit Union, Delta Community Credit Union, NBT Bank, Seattle Metropolitan Credit Union, Popular, Oriental, American National Bank, Ameris Bank, Community Bank of the South, East West Bank, Worldwide, Bank of the Southwest, Bank of Colorado, Bremer Bank, Horizon Bank, Hantle, Tampa Bay Federal Credit Union, iQ Credit Union, Bank Independent, University Federal Credit Union, BMO Harris, First Community Credit Union, The Huntington National Bank, Michigan Tech Employees Federal Credit Union, BBVA Compass Bank, Bank of America ATM, United Bank of Union, Bank of Sullivan, Western Union, 'ATM Network, Inc.', Citibank ATM, Cashpoints, SchoolsFirst, WSECU, Citizens National Bank, State Employees' Credit Union (ATM), VyStar, State Employee Credit Union (ATM), CVS, ATM (Wells Fargo Bank), North Shore Bank, PNC Bank ATM, Bitcoin Depot ATM, Visions Federal Credit Union, City ATM, Cypress Advantage, Well's Fargo, JP Morgan Chase, Cornerstone Bank, PSECU, Partners Federal Credit Union, M & T Bank, First Niagara Bank, Webster, UMB, Bank of Oklahoma, AZ Federal Credit Union, First Niagara, WSFS, Credit Union of Colorado, Town Bank, First Security Bank, Digital Credit Union, Public Service Credit Union, Bank of Texas, Jersey Shore State Bank, Eastman Credit Union, Johns Hopkins Federal Credit Union, United Bank, usbank, Univest, Penn Community Bank, Mountain America Credit Union, Home Federal, Nusenda Credit Union, Leonardville State Bank, Village Pointe, Bank of New Hampshire, Presto!, Peoples Bank, WECU, Bank Midwest, Caltech Employees Federal Credit Union, First American Bank, Exchange Bank, Washington State Employees Credit Union, Kennebec Savings Bank, Fidelity Bank, First National Bank of Northern California, Triangle Credit Union, University of Colorado Colorado Springs, Ent Credit Union, Fifth Third, Valley National Bank, Banterra Bank, Morton Community Bank, 5/3, SECU, CorTrust Bank, Home Federal Bank, First Dakota National Bank, Old Second Bank, Farmers & Merchants State Bank, Cal-Ed Federal Credit Union, Security First Bank, Elevations Credit Union, Busey, Veridian Credit Union, Well Fargo, Wells Fargo (Perm. Closed), Kitsap Credit Union, Bank of North Georgia, LGE Community Credit Union, Capital Bank, Community Resource Bank, Sun Trust, BMO, Southern Mississippi Federal Credit Union, TD, Coinucopia.io, Patelco Credit Union, Alliant Credit Union, Fort Campbell Federal Credit Union, FNBT, Sunflower Bank, XBTeller, Canandaigua National Bank & Trust, Glass City Federal Credit Union, Purdue Federal Credit Union, Riverview Community Bank, NC State Employees Credit Union, First Tech Federal Credit Union, First Commonwealth Bank, United Bank & Trust, Citizens State Bank, Oregon Community Credit Union | OCCU, Columbia Credit Union, 'Payment Alliance International, Inc.', INB, Bank Iowa, Oriental Bank, First Financial Northwest Bank, Ohio University Credit Union, Truist, Opus Bank, Happy State Bank, Mainstreet Credit Union, Bitcoin ATM, WestAmerica Bank, Affinity, Community Banks of Colorado, Banco Popular de Puerto Rico, First National Santa Fe, Bank of Franklin County, Reliance Bank, TFCU, North Carolina State Employees' Credit Union, InRoads Credit Union, Premier, Credit Union of Georgia, Seattle Credit Union, EcoATM, Frost Bank, Hills Bank and Trust Company, Sidney Federal Credit Union, Beacon Credit Union, BECU (Burien Plaza), HSBC, ATM Express, Blue Eagle Credit Union, Express Mart, Valley National Bank ATM, Santander Bank ATM, ATM (TD Bank), NC State Employees' Credit Union, Canvas Credit Union, Heartland State Bank, Tri Counties Bank, The First, First Commonweaith, Guadalupe Credit Union, Trustco Bank, Stanford Federal Credit Union, Century Bank, U.S. Bank ATM, LANB, Freedom Federal Credit Union, Altra Federal Credit Union, First-Knox National Bank, Xplore, Genmega, 1st Source Bank, Terre Haute Savings Bank, Cash Points (ATM), Bank OZK, Northwest Bank, Red Canoe, Broadway Bank, Educators Credit Union, ChaseBank, Centra Credit Union, Richton Bank & Trust Company]
 centerLineStyle: yellow
 exclusiveCycleLaneStyle: white
+hasAdvisoryCycleLane: false
 hasAdvisorySpeedLimitSign: true
 hasCenterLeftTurnLane: true
 hasNoStandingSign: true
diff --git a/app/src/main/assets/country_metadata/default.yml b/app/src/main/assets/country_metadata/default.yml
index 72efd9cd982..ae97e041d03 100644
--- a/app/src/main/assets/country_metadata/default.yml
+++ b/app/src/main/assets/country_metadata/default.yml
@@ -7,6 +7,7 @@ firstDayOfWorkweek: Mo
 hasAdvisoryCycleLane: false
 hasAdvisorySpeedLimitSign: false
 hasBiWeeklyAlternateSideParkingSign: false
+hasBicycleBoulevard: false
 hasCenterLeftTurnLane: false
 hasDailyAlternateSideParkingSign: false
 hasLivingStreet: false
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/meta/CountryInfo.kt b/app/src/main/java/de/westnordost/streetcomplete/data/meta/CountryInfo.kt
index d4942e18223..1ec2d2bb634 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/meta/CountryInfo.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/meta/CountryInfo.kt
@@ -49,6 +49,7 @@ data class IncompleteCountryInfo(
     val hasBiWeeklyAlternateSideParkingSign: Boolean? = null,
     val hasCenterLeftTurnLane: Boolean? = null,
     val hasAdvisoryCycleLane: Boolean? = null,
+    val hasBicycleBoulevard: Boolean? = null,
     val hasDailyAlternateSideParkingSign: Boolean? = null,
     val hasLivingStreet: Boolean? = null,
     val hasNoStandingSign: Boolean? = null,
@@ -100,6 +101,8 @@ data class CountryInfo(private val infos: List<IncompleteCountryInfo>) {
         get() = infos.firstNotNullOf { it.hasAdvisoryCycleLane }
     val hasAdvisorySpeedLimitSign: Boolean
         get() = infos.firstNotNullOf { it.hasAdvisorySpeedLimitSign }
+    val hasBicycleBoulevard: Boolean
+        get() = infos.firstNotNullOf { it.hasBicycleBoulevard }
     val hasBiWeeklyAlternateSideParkingSign: Boolean
         get() = infos.firstNotNullOf { it.hasBiWeeklyAlternateSideParkingSign }
     val hasCenterLeftTurnLane: Boolean
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmElementQuestType.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmElementQuestType.kt
index dbf1e288ede..d5627ef88ed 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmElementQuestType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmElementQuestType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.data.osm.osmquests
 
 import de.westnordost.streetcomplete.data.osm.edits.ElementEditType
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.quest.AllCountries
@@ -96,9 +97,10 @@ interface OsmElementQuestType<T> : QuestType, ElementEditType {
      *  any misunderstandings which element is meant that far apart. */
     val highlightedElementsRadius: Double get() = 30.0
 
-    /** applies the data from [answer] to the element that has last been edited at [timestampEdited].
-     * The element is not directly modified, instead, a map of [tags] is built */
-    fun applyAnswerTo(answer: T, tags: Tags, timestampEdited: Long)
+    /** Applies the data from [answer] to the element that has last been edited at [timestampEdited]
+     * with the given [tags] and the given [geometry].
+     * The element is not directly modified, instead, a map of [tags] is modified */
+    fun applyAnswerTo(answer: T, tags: Tags, geometry: ElementGeometry, timestampEdited: Long)
 
     override fun createForm(): AbstractOsmQuestForm<T>
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/achievements/AchievementsModule.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/achievements/AchievementsModule.kt
index 400e8318de6..4b224ac4658 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/achievements/AchievementsModule.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/achievements/AchievementsModule.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.data.user.achievements
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.overlays.street_parking.StreetParkingOverlay
+import de.westnordost.streetcomplete.quests.cycleway.AddCycleway
 import de.westnordost.streetcomplete.quests.foot.AddProhibitedForPedestrians
 import de.westnordost.streetcomplete.quests.oneway.AddOneway
 import de.westnordost.streetcomplete.quests.sidewalk.AddSidewalk
@@ -51,6 +52,7 @@ private val typeAliases = listOf(
     // whether lit roads have been added in context of the quest or the overlay should not matter for the statistics
     "WayLitOverlay"                      to AddWayLit::class.simpleName!!,
     "SidewalkOverlay"                    to AddSidewalk::class.simpleName!!,
+    "CyclewayOverlay"                    to AddCycleway::class.simpleName!!,
     "AddStreetParking"                   to StreetParkingOverlay::class.simpleName!!
 )
 
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/Oneway.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/Oneway.kt
index 9f0854fe945..4ddfa1bb6e9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/Oneway.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/Oneway.kt
@@ -17,3 +17,14 @@ fun isNotOnewayForCyclists(tags: Map<String, String>, isLeftHandTraffic: Boolean
     || tags[
         if (isLeftHandTraffic xor isReversedOneway(tags)) "cycleway:right" else "cycleway:left"
     ]?.startsWith("opposite") == true
+
+/** Return whether the given side is in the contra-flow of a oneway. E.g. in Germany for a forward
+ *  oneway, it is the left side */
+fun isInContraflowOfOneway(
+    isRightSide: Boolean,
+    tags: Map<String, String>,
+    isLeftHandTraffic: Boolean
+): Boolean {
+    val isReverseSideRight = isReversedOneway(tags) xor isLeftHandTraffic
+    return isOneway(tags) && isReverseSideRight == isRightSide
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/RoadWidth.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/RoadWidth.kt
index 8ed0eeba785..305d615ed72 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/RoadWidth.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/RoadWidth.kt
@@ -100,12 +100,12 @@ fun estimateCycleTrackWidth(tags: Map<String, String>): Float? =
 private fun estimateCyclewaysWidth(tags: Map<String, String>, isLane: Boolean): Float? {
     val sides = createCyclewaySides(tags, false) ?: return null
 
-    val leftWidth = if (sides.left?.isLane == isLane) {
+    val leftWidth = if (sides.left?.cycleway?.isLane == isLane) {
         (tags["cycleway:both:width"] ?: tags["cycleway:left:width"])?.toFloatOrNull()
             ?: sides.left.estimatedWidth
     } else 0f
 
-    val rightWidth = if (sides.right?.isLane == isLane) {
+    val rightWidth = if (sides.right?.cycleway?.isLane == isLane) {
         (tags["cycleway:both:width"] ?: tags["cycleway:right:width"])?.toFloatOrNull()
             ?: sides.right.estimatedWidth
     } else 0f
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/Surface.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/Surface.kt
index 7fdaaa3caf4..93ed76c6622 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/Surface.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/Surface.kt
@@ -1,6 +1,8 @@
 package de.westnordost.streetcomplete.osm
 
-val SOFT_SURFACES = setOf("ground", "earth", "dirt", "grass", "sand", "mud", "ice", "salt", "snow", "woodchips")
+val SOFT_SURFACES = setOf(
+    "ground", "earth", "dirt", "grass", "sand", "mud", "ice", "salt", "snow", "woodchips"
+)
 
 val ANYTHING_UNPAVED = SOFT_SURFACES + setOf(
     "unpaved", "compacted", "gravel", "fine_gravel", "pebblestone", "grass_paver",
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/Tags.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/Tags.kt
index dd662b23ace..d4f18f1d962 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/Tags.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/Tags.kt
@@ -3,3 +3,26 @@ package de.westnordost.streetcomplete.osm
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
 
 typealias Tags = StringMapChangesBuilder
+
+fun Tags.expandSides(key: String, postfix: String? = null, includeBareTag: Boolean = true) {
+    val post = if (postfix != null) ":$postfix" else ""
+    val both = get("$key:both$post") ?: (if (includeBareTag) get("$key$post") else null)
+    if (both != null) {
+        // *:left/right is seen as more specific/correct in case the two contradict each other
+        if (!containsKey("$key:left$post")) set("$key:left$post", both)
+        if (!containsKey("$key:right$post")) set("$key:right$post", both)
+    }
+    remove("$key:both$post")
+    if (includeBareTag) remove("$key$post")
+}
+
+fun Tags.mergeSides(key: String, postfix: String? = null) {
+    val post = if (postfix != null) ":$postfix" else ""
+    val left = get("$key:left$post")
+    val right = get("$key:right$post")
+    if (left != null && left == right) {
+        set("$key:both$post", left)
+        remove("$key:left$post")
+        remove("$key:right$post")
+    }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/bicycle_boulevard/BicycleBoulevard.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/bicycle_boulevard/BicycleBoulevard.kt
new file mode 100644
index 00000000000..5838b305046
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/bicycle_boulevard/BicycleBoulevard.kt
@@ -0,0 +1,36 @@
+package de.westnordost.streetcomplete.osm.bicycle_boulevard
+
+import de.westnordost.streetcomplete.osm.Tags
+import de.westnordost.streetcomplete.osm.bicycle_boulevard.BicycleBoulevard.*
+
+enum class BicycleBoulevard { YES, NO }
+
+fun createBicycleBoulevard(tags: Map<String, String>): BicycleBoulevard =
+    // "no" value is extremely uncommon, hence the lack of this tag can be interpreted as "no"
+    if (tags["bicycle_road"] == "yes" || tags["cyclestreet"] == "yes") YES else NO
+
+fun BicycleBoulevard.applyTo(tags: Tags, countryCode: String) {
+    when (this) {
+        YES -> {
+            // do not re-tag to different key if one already exists
+            val useBicycleRoad = when {
+                tags.containsKey("bicycle_road") -> true
+                tags.containsKey("cyclestreet") -> false
+                // in BeNeLux countries, cyclestreet established itself instead
+                countryCode in listOf("BE", "NL", "LU") -> false
+                else -> true
+            }
+            if (useBicycleRoad) {
+                tags["bicycle_road"] = "yes"
+                tags.remove("cyclestreet")
+            } else {
+                tags["cyclestreet"] = "yes"
+                tags.remove("bicycle_road")
+            }
+        }
+        NO -> {
+            tags.remove("bicycle_road")
+            tags.remove("cyclestreet")
+        }
+    }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/Cycleway.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/Cycleway.kt
index 8470cb85cba..4b71658bbcf 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/Cycleway.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/Cycleway.kt
@@ -1,16 +1,80 @@
 package de.westnordost.streetcomplete.osm.cycleway
 
+import kotlinx.serialization.Serializable
 import de.westnordost.streetcomplete.data.meta.CountryInfo
 import de.westnordost.streetcomplete.osm.cycleway.Cycleway.*
+import de.westnordost.streetcomplete.osm.cycleway.Direction.*
+import de.westnordost.streetcomplete.osm.isForwardOneway
+import de.westnordost.streetcomplete.osm.isInContraflowOfOneway
+import de.westnordost.streetcomplete.osm.isNotOnewayForCyclists
+import de.westnordost.streetcomplete.osm.isOneway
+import de.westnordost.streetcomplete.osm.isReversedOneway
+
+data class LeftAndRightCycleway(val left: CyclewayAndDirection?, val right: CyclewayAndDirection?)
+
+fun LeftAndRightCycleway.any(block: (cycleway: CyclewayAndDirection) -> Boolean): Boolean =
+    left?.let(block) == true || right?.let(block) == true
+
+fun LeftAndRightCycleway.selectableOrNullValues(countryInfo: CountryInfo): LeftAndRightCycleway {
+    val leftIsSelectable = left?.isSelectable(countryInfo) != false
+    val rightIsSelectable = right?.isSelectable(countryInfo) != false
+    if (leftIsSelectable && rightIsSelectable) return this
+    return LeftAndRightCycleway(
+        if (leftIsSelectable) left else null,
+        if (rightIsSelectable) right else null
+    )
+}
+
+fun LeftAndRightCycleway.wasNoOnewayForCyclistsButNowItIs(tags: Map<String, String>, isLeftHandTraffic: Boolean): Boolean =
+    isOneway(tags)
+    && isNotOnewayForCyclists(tags, isLeftHandTraffic)
+    && !isNotOnewayForCyclistsNow(tags, isLeftHandTraffic)
+
+fun LeftAndRightCycleway.isNotOnewayForCyclistsNow(tags: Map<String, String>, isLeftHandTraffic: Boolean): Boolean {
+    val onewayDir = when  {
+        isForwardOneway(tags) -> FORWARD
+        isReversedOneway(tags) -> BACKWARD
+        else -> return true
+    }
+    val previous = createCyclewaySides(tags, isLeftHandTraffic)
+    val l = (left ?: previous?.left)
+    val r = (right ?: previous?.right)
+    // "no cycleway" has no direction and should be ignored
+    val leftDir = l?.direction?.takeIf { l.cycleway != NONE }
+    val rightDir = r?.direction?.takeIf { r.cycleway != NONE }
+
+    return leftDir == BOTH || rightDir == BOTH ||
+        leftDir != null && leftDir != onewayDir ||
+        rightDir != null && rightDir != onewayDir
+}
 
+@Serializable
+data class CyclewayAndDirection(val cycleway: Cycleway, val direction: Direction)
+
+fun CyclewayAndDirection.isSelectable(countryInfo: CountryInfo): Boolean =
+    cycleway.isSelectable(countryInfo) &&
+    // only allow dual track and dual lanes (not dual pictograms or something)
+    (direction != BOTH || cycleway == TRACK || cycleway == UNSPECIFIED_LANE || cycleway == EXCLUSIVE_LANE)
+
+@Serializable
+enum class Direction {
+    FORWARD, BACKWARD, BOTH;
+
+    fun reverse(): Direction = when (this) {
+        FORWARD -> BACKWARD
+        BACKWARD -> FORWARD
+        BOTH -> BOTH
+    }
+}
+
+@Serializable
 enum class Cycleway {
     /** a.k.a. cycle lane with continuous markings, dedicated lane or simply (proper) lane. Usually
      *  exclusive access for cyclists */
     EXCLUSIVE_LANE,
-    /** [EXCLUSIVE_LANE] in both directions */
-    DUAL_LANE,
-    /** a.k.a. cycle lane with dashed markings, protective lane, multipurpose lane, soft lane or
-     *  recommended lane. Usually priority access for cyclists, cars may use if necessary */
+    /** a.k.a. cycle lane with dashed markings, protective lane, multipurpose lane, soft lane,
+     *  recommended lane or cycle lanes on 2-1 roads. Usually priority access for cyclists, cars
+     *  may use if necessary */
     ADVISORY_LANE,
     /** some kind of cycle lane, not specified whether exclusive or advisory */
     UNSPECIFIED_LANE,
@@ -30,8 +94,6 @@ enum class Cycleway {
 
     /** cycle track, i.e. beyond curb or other barrier */
     TRACK,
-    /** [TRACK] in both directions */
-    DUAL_TRACK,
 
     /** cyclists share space with bus lane */
     BUSWAY,
@@ -52,6 +114,9 @@ enum class Cycleway {
     /** cycleway is mapped as a separate way */
     SEPARATE,
 
+    /** no cycle track or lane, but cyclists use shoudler */
+    SHOULDER,
+
     /** unknown cycleway tag set */
     UNKNOWN,
 
@@ -64,7 +129,7 @@ enum class Cycleway {
     /** is a lane (cycleway=lane or cycleway=shared_lane), shared on busway doesn't count as a lane
      *  in that sense because it is not a subtag of the mentioned tags */
     val isLane get() = when (this) {
-        EXCLUSIVE_LANE, DUAL_LANE, ADVISORY_LANE, UNSPECIFIED_LANE, UNKNOWN_LANE,
+        EXCLUSIVE_LANE, ADVISORY_LANE, UNSPECIFIED_LANE, UNKNOWN_LANE,
         SUGGESTION_LANE, PICTOGRAMS, UNSPECIFIED_SHARED_LANE, UNKNOWN_SHARED_LANE -> true
         else -> false
     }
@@ -74,11 +139,17 @@ enum class Cycleway {
         else -> false
     }
 
-    val isInvalid get() = this == INVALID
+    val isReversible get() = when (this) {
+        NONE, NONE_NO_ONEWAY, SEPARATE, SHOULDER -> false
+        else -> true
+    }
 
-    val isOneway get() = this != DUAL_LANE && this != DUAL_TRACK
+    val isInvalid get() = this == INVALID
 }
 
+private fun Cycleway.isSelectable(countryInfo: CountryInfo): Boolean =
+    !isInvalid && !isUnknown && !isAmbiguous(countryInfo)
+
 fun Cycleway.isAmbiguous(countryInfo: CountryInfo) = when (this) {
     UNSPECIFIED_SHARED_LANE ->
         true
@@ -88,7 +159,12 @@ fun Cycleway.isAmbiguous(countryInfo: CountryInfo) = when (this) {
         false
 }
 
-fun getSelectableCyclewaysInCountry(countryInfo: CountryInfo): List<Cycleway> {
+fun getSelectableCycleways(
+    countryInfo: CountryInfo,
+    roadTags: Map<String, String>,
+    isRightSide: Boolean
+): List<CyclewayAndDirection> {
+    val dir = if (isRightSide xor countryInfo.isLeftHandTraffic) FORWARD else BACKWARD
     val cycleways = mutableListOf(
         NONE,
         TRACK,
@@ -100,9 +176,15 @@ fun getSelectableCyclewaysInCountry(countryInfo: CountryInfo): List<Cycleway> {
         PICTOGRAMS,
         BUSWAY,
         SIDEWALK_EXPLICIT,
-        DUAL_LANE,
-        DUAL_TRACK
+        SHOULDER
+    )
+    val dualCycleways = listOf(
+        CyclewayAndDirection(if (countryInfo.hasAdvisoryCycleLane) EXCLUSIVE_LANE else UNSPECIFIED_LANE, BOTH),
+        CyclewayAndDirection(TRACK, BOTH),
     )
+
+    // no need to distinguish between advisory and exclusive lane where the concept of exclusive
+    // lanes does not exist
     if (countryInfo.hasAdvisoryCycleLane) {
         cycleways.remove(UNSPECIFIED_LANE)
     } else {
@@ -115,17 +197,20 @@ fun getSelectableCyclewaysInCountry(countryInfo: CountryInfo): List<Cycleway> {
     } else {
         cycleways.remove(SUGGESTION_LANE)
     }
-    return cycleways
+    // different wording for a contraflow lane that is marked like a "shared" lane (just bicycle pictogram)
+    if (isInContraflowOfOneway(isRightSide, roadTags, countryInfo.isLeftHandTraffic)) {
+        cycleways.remove(PICTOGRAMS)
+        cycleways.add(cycleways.indexOf(NONE) + 1, NONE_NO_ONEWAY)
+    }
+    return cycleways.map { CyclewayAndDirection(it, dir) } + dualCycleways
 }
 
-val Cycleway.estimatedWidth: Float get() = when (this) {
+val CyclewayAndDirection.estimatedWidth: Float get() = when (cycleway) {
     EXCLUSIVE_LANE -> 1.5f
-    DUAL_LANE -> 3f
     ADVISORY_LANE -> 1f
     UNSPECIFIED_LANE -> 1f
     UNKNOWN_LANE -> 1f
     SUGGESTION_LANE -> 0.75f
     TRACK -> 1.5f
-    DUAL_TRACK -> 3f
     else -> 0f
-}
+} * if (direction == BOTH) 2 else 1
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreator.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreator.kt
new file mode 100644
index 00000000000..cd1049701ef
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreator.kt
@@ -0,0 +1,155 @@
+package de.westnordost.streetcomplete.osm.cycleway
+
+import de.westnordost.streetcomplete.osm.cycleway.Cycleway.*
+import de.westnordost.streetcomplete.osm.Tags
+import de.westnordost.streetcomplete.osm.cycleway.Direction.*
+import de.westnordost.streetcomplete.osm.expandSides
+import de.westnordost.streetcomplete.osm.hasCheckDateForKey
+import de.westnordost.streetcomplete.osm.isInContraflowOfOneway
+import de.westnordost.streetcomplete.osm.isOneway
+import de.westnordost.streetcomplete.osm.mergeSides
+import de.westnordost.streetcomplete.osm.sidewalk.LeftAndRightSidewalk
+import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk
+import de.westnordost.streetcomplete.osm.sidewalk.applyTo
+import de.westnordost.streetcomplete.osm.updateCheckDateForKey
+
+fun LeftAndRightCycleway.applyTo(tags: Tags, isLeftHandTraffic: Boolean) {
+    if (left == null && right == null) return
+    /* for being able to modify only one side (e.g. `left` is null while `right` is not null),
+       the sides conflated in `:both` keys need to be separated first. E.g. `cycleway=no`
+       when left side is made `separate` should become
+       - cycleway:right=no
+       - cycleway:left=separate
+       First separating the values and then later conflating them again, if possible, solves this.
+     */
+    tags.expandSides("cycleway")
+    tags.expandSides("cycleway", "lane")
+    tags.expandSides("cycleway", "oneway")
+    tags.expandSides("cycleway", "segregated")
+
+    applyOnewayNotForCyclists(tags, isLeftHandTraffic)
+    left?.applyTo(tags, false, isLeftHandTraffic)
+    right?.applyTo(tags, true, isLeftHandTraffic)
+
+    tags.mergeSides("cycleway")
+    tags.mergeSides("cycleway", "lane")
+    tags.mergeSides("cycleway", "oneway")
+    tags.mergeSides("cycleway", "segregated")
+
+    // update check date
+    if (!tags.hasChanges || tags.hasCheckDateForKey("cycleway")) {
+        tags.updateCheckDateForKey("cycleway")
+    }
+
+    // tag sidewalk after setting the check date because we want to primarily set the check date
+    // of the cycleway, not of the sidewalk, if there are no changes
+    LeftAndRightSidewalk(
+        if (left?.cycleway == SIDEWALK_EXPLICIT) Sidewalk.YES else null,
+        if (right?.cycleway == SIDEWALK_EXPLICIT) Sidewalk.YES else null,
+    ).applyTo(tags)
+}
+
+private fun LeftAndRightCycleway.applyOnewayNotForCyclists(tags: Tags, isLeftHandTraffic: Boolean) {
+    if (isOneway(tags) && isNotOnewayForCyclistsNow(tags, isLeftHandTraffic)) {
+        tags["oneway:bicycle"] = "no"
+    } else {
+        if (tags["oneway:bicycle"] == "no") {
+            tags.remove("oneway:bicycle")
+        }
+    }
+}
+
+private fun CyclewayAndDirection.applyTo(tags: Tags, isRight: Boolean, isLeftHandTraffic: Boolean) {
+    val side = if (isRight) "right" else "left"
+    val cyclewayKey = "cycleway:$side"
+    when (cycleway) {
+        NONE, NONE_NO_ONEWAY -> {
+            tags[cyclewayKey] = "no"
+        }
+        UNSPECIFIED_LANE -> {
+            tags[cyclewayKey] = "lane"
+            // does not remove any cycleway:lane tag because this value is not considered explicit
+        }
+        ADVISORY_LANE -> {
+            tags[cyclewayKey] = "lane"
+            tags["$cyclewayKey:lane"] = "advisory"
+        }
+        EXCLUSIVE_LANE -> {
+            tags[cyclewayKey] = "lane"
+            tags["$cyclewayKey:lane"] = "exclusive"
+        }
+        TRACK -> {
+            tags[cyclewayKey] = "track"
+            // only set if already set, because "yes" is considered the implicit default
+            if (tags.containsKey("$cyclewayKey:segregated")) {
+                tags["$cyclewayKey:segregated"] = "yes"
+            }
+        }
+        SIDEWALK_EXPLICIT -> {
+            // https://wiki.openstreetmap.org/wiki/File:Z240GemeinsamerGehundRadweg.jpeg
+            tags[cyclewayKey] = "track"
+            tags["$cyclewayKey:segregated"] = "no"
+        }
+        PICTOGRAMS -> {
+            tags[cyclewayKey] = "shared_lane"
+            tags["$cyclewayKey:lane"] = "pictogram"
+        }
+        SUGGESTION_LANE -> {
+            tags[cyclewayKey] = "shared_lane"
+            tags["$cyclewayKey:lane"] = "advisory"
+        }
+        UNSPECIFIED_SHARED_LANE -> {
+            tags[cyclewayKey] = "shared_lane"
+            // does not remove any cycleway:lane tag because value is not considered explicit
+        }
+        BUSWAY -> {
+            tags[cyclewayKey] = "share_busway"
+        }
+        SHOULDER -> {
+            tags[cyclewayKey] = "shoulder"
+        }
+        SEPARATE -> {
+            tags[cyclewayKey] = "separate"
+        }
+        else -> {
+            throw IllegalArgumentException("Invalid cycleway")
+        }
+    }
+
+    val isPhysicalCycleway = cycleway != NONE && cycleway != SEPARATE && cycleway != NONE_NO_ONEWAY
+    if (!isPhysicalCycleway) {
+        tags.remove("$cyclewayKey:oneway")
+    } else {
+        /* explicitly tag cycleway direction when either
+           - the selected direction is not the one assumed by default when the key is missing
+           - or the road is a oneway and the cycleway is on the contra-flow side
+           - or it is already tagged (to correct it if need be)
+         */
+        val isDefaultDirection = when (direction) {
+            FORWARD -> isRight xor isLeftHandTraffic
+            BACKWARD -> !isRight xor isLeftHandTraffic
+            BOTH -> false
+        }
+        val isInContraflowOfOneway = isInContraflowOfOneway(isRight, tags, isLeftHandTraffic)
+        if (!isDefaultDirection || isInContraflowOfOneway || tags.containsKey("$cyclewayKey:oneway")) {
+            tags["$cyclewayKey:oneway"] = direction.onewayValue
+        }
+    }
+
+    // clear previous cycleway:lane value
+    if (!cycleway.isLane) {
+        tags.remove("$cyclewayKey:lane")
+    }
+
+    // clear previous cycleway:segregated value
+    val touchedSegregatedValue = cycleway in listOf(SIDEWALK_EXPLICIT, TRACK)
+    if (!touchedSegregatedValue) {
+        tags.remove("$cyclewayKey:segregated")
+    }
+}
+
+private val Direction.onewayValue get() = when (this) {
+    FORWARD -> "yes"
+    BACKWARD -> "-1"
+    BOTH -> "no"
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayItem.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayItem.kt
new file mode 100644
index 00000000000..1520f263f73
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayItem.kt
@@ -0,0 +1,160 @@
+package de.westnordost.streetcomplete.osm.cycleway
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.drawable.Drawable
+import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.meta.CountryInfo
+import de.westnordost.streetcomplete.osm.cycleway.Cycleway.*
+import de.westnordost.streetcomplete.osm.cycleway.Direction.*
+import de.westnordost.streetcomplete.util.ktx.advisoryCycleLaneResId
+import de.westnordost.streetcomplete.util.ktx.advisoryCycleLaneMirroredResId
+import de.westnordost.streetcomplete.util.ktx.dualCycleLaneMirroredResId
+import de.westnordost.streetcomplete.util.ktx.dualCycleLaneResId
+import de.westnordost.streetcomplete.util.ktx.exclusiveCycleLaneMirroredResId
+import de.westnordost.streetcomplete.util.ktx.exclusiveCycleLaneResId
+import de.westnordost.streetcomplete.util.ktx.noEntrySignDrawableResId
+import de.westnordost.streetcomplete.util.ktx.pictogramCycleLaneMirroredResId
+import de.westnordost.streetcomplete.util.ktx.pictogramCycleLaneResId
+import de.westnordost.streetcomplete.view.DrawableImage
+import de.westnordost.streetcomplete.view.DrawableWrapper
+import de.westnordost.streetcomplete.view.Image
+import de.westnordost.streetcomplete.view.ResImage
+import de.westnordost.streetcomplete.view.ResText
+import de.westnordost.streetcomplete.view.controller.StreetSideItem
+import de.westnordost.streetcomplete.view.image_select.Item2
+
+fun CyclewayAndDirection.asDialogItem(
+    isRight: Boolean,
+    isContraflowInOneway: Boolean,
+    context: Context,
+    countryInfo: CountryInfo
+) =
+    Item2(
+        this,
+        getDialogIcon(context, isRight, countryInfo),
+        ResText(getTitleResId(isContraflowInOneway))
+    )
+
+fun CyclewayAndDirection.asStreetSideItem(
+    isRight: Boolean,
+    isContraflowInOneway: Boolean,
+    countryInfo: CountryInfo
+) =
+    StreetSideItem(
+        this,
+        getIconResId(isRight, countryInfo),
+        getTitleResId(isContraflowInOneway),
+        getDialogIconResId(isRight, countryInfo),
+        cycleway.getFloatingIconResId(isContraflowInOneway, countryInfo.noEntrySignDrawableResId)
+    )
+
+private fun CyclewayAndDirection.getDialogIcon(
+    context: Context,
+    isRight: Boolean,
+    countryInfo: CountryInfo
+): Image {
+    val id = getDialogIconResId(isRight, countryInfo)
+    return if (countryInfo.isLeftHandTraffic) {
+        DrawableImage(Rotate180Degrees(context.getDrawable(id)!!))
+    } else {
+        ResImage(id)
+    }
+}
+
+private fun CyclewayAndDirection.getDialogIconResId(isRight: Boolean, countryInfo: CountryInfo): Int =
+    when (cycleway) {
+        NONE ->     R.drawable.ic_cycleway_none_in_selection
+        SEPARATE -> R.drawable.ic_cycleway_separate
+        else ->     getIconResId(isRight, countryInfo)
+    }
+
+private class Rotate180Degrees(drawable: Drawable) : DrawableWrapper(drawable) {
+    override fun draw(canvas: Canvas) {
+        canvas.scale(-1f, -1f, bounds.width() / 2f, bounds.height() / 2f)
+        drawable.bounds = bounds
+        drawable.draw(canvas)
+    }
+}
+
+private fun Cycleway.getFloatingIconResId(isContraflowInOneway: Boolean, noEntrySignDrawableResId: Int): Int? = when (this) {
+    NONE ->     if (isContraflowInOneway) noEntrySignDrawableResId else null
+    SEPARATE -> R.drawable.ic_sidewalk_floating_separate
+    else ->     null
+}
+
+private fun CyclewayAndDirection.getIconResId(isRight: Boolean, countryInfo: CountryInfo): Int = when (direction) {
+    BOTH -> cycleway.getDualTrafficIconResId(countryInfo)
+    else -> {
+        val isForward = (direction == FORWARD)
+        val showMirrored = isForward xor isRight
+        if (showMirrored) cycleway.getLeftHandTrafficIconResId(countryInfo)
+        else              cycleway.getRightHandTrafficIconResId(countryInfo)
+    }
+}
+
+private fun Cycleway.getDualTrafficIconResId(countryInfo: CountryInfo): Int = when (this) {
+    UNSPECIFIED_LANE, EXCLUSIVE_LANE ->
+        if (countryInfo.isLeftHandTraffic) countryInfo.dualCycleLaneMirroredResId
+        else                               countryInfo.dualCycleLaneResId
+    TRACK ->
+        if (countryInfo.isLeftHandTraffic) R.drawable.ic_cycleway_track_dual_l
+        else                               R.drawable.ic_cycleway_track_dual
+    else -> 0
+}
+
+private fun Cycleway.getRightHandTrafficIconResId(countryInfo: CountryInfo): Int = when (this) {
+    UNSPECIFIED_LANE ->  countryInfo.exclusiveCycleLaneResId
+    EXCLUSIVE_LANE ->    countryInfo.exclusiveCycleLaneResId
+    ADVISORY_LANE ->     countryInfo.advisoryCycleLaneResId
+    SUGGESTION_LANE ->   countryInfo.advisoryCycleLaneResId
+    TRACK ->             R.drawable.ic_cycleway_track
+    NONE ->              R.drawable.ic_cycleway_none
+    NONE_NO_ONEWAY ->    R.drawable.ic_cycleway_none_no_oneway
+    PICTOGRAMS ->        countryInfo.pictogramCycleLaneResId
+    SIDEWALK_EXPLICIT -> R.drawable.ic_cycleway_sidewalk_explicit
+    BUSWAY ->            R.drawable.ic_cycleway_bus_lane
+    SEPARATE ->          R.drawable.ic_cycleway_none
+    SHOULDER ->          R.drawable.ic_cycleway_shoulder
+    else -> 0
+}
+
+private fun Cycleway.getLeftHandTrafficIconResId(countryInfo: CountryInfo): Int = when (this) {
+    UNSPECIFIED_LANE ->  countryInfo.exclusiveCycleLaneMirroredResId
+    EXCLUSIVE_LANE ->    countryInfo.exclusiveCycleLaneMirroredResId
+    ADVISORY_LANE ->     countryInfo.advisoryCycleLaneMirroredResId
+    SUGGESTION_LANE ->   countryInfo.advisoryCycleLaneMirroredResId
+    TRACK ->             R.drawable.ic_cycleway_track_l
+    NONE ->              R.drawable.ic_cycleway_none
+    NONE_NO_ONEWAY ->    R.drawable.ic_cycleway_none_no_oneway_l
+    PICTOGRAMS ->        countryInfo.pictogramCycleLaneMirroredResId
+    SIDEWALK_EXPLICIT -> R.drawable.ic_cycleway_sidewalk_explicit_l
+    BUSWAY ->            R.drawable.ic_cycleway_bus_lane_l
+    SEPARATE ->          R.drawable.ic_cycleway_none
+    SHOULDER ->          R.drawable.ic_cycleway_shoulder
+    else -> 0
+}
+
+private fun CyclewayAndDirection.getTitleResId(isContraflowInOneway: Boolean): Int = when (cycleway) {
+    UNSPECIFIED_LANE, EXCLUSIVE_LANE -> {
+        if (direction == BOTH) R.string.quest_cycleway_value_lane_dual
+        else                   R.string.quest_cycleway_value_lane
+    }
+    TRACK -> {
+        if (direction == BOTH) R.string.quest_cycleway_value_track_dual
+        else                   R.string.quest_cycleway_value_track
+    }
+    NONE -> {
+        if (isContraflowInOneway) R.string.quest_cycleway_value_none_and_oneway
+        else                      R.string.quest_cycleway_value_none
+    }
+    ADVISORY_LANE,
+    SUGGESTION_LANE ->   R.string.quest_cycleway_value_advisory_lane
+    NONE_NO_ONEWAY ->    R.string.quest_cycleway_value_none_but_no_oneway
+    PICTOGRAMS ->        R.string.quest_cycleway_value_shared
+    SIDEWALK_EXPLICIT -> R.string.quest_cycleway_value_sidewalk
+    BUSWAY ->            R.string.quest_cycleway_value_bus_lane
+    SEPARATE ->          R.string.quest_cycleway_value_separate
+    SHOULDER ->          R.string.quest_cycleway_value_shoulder
+    else -> 0
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParser.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParser.kt
index 99217017044..dfd3fe59ed5 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParser.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParser.kt
@@ -1,33 +1,14 @@
 package de.westnordost.streetcomplete.osm.cycleway
 
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.ADVISORY_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.BUSWAY
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.DUAL_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.DUAL_TRACK
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.EXCLUSIVE_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.INVALID
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.NONE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.NONE_NO_ONEWAY
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.PICTOGRAMS
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SEPARATE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SIDEWALK_EXPLICIT
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SUGGESTION_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.TRACK
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNKNOWN
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNKNOWN_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNKNOWN_SHARED_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNSPECIFIED_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNSPECIFIED_SHARED_LANE
+import de.westnordost.streetcomplete.osm.cycleway.Cycleway.*
 import de.westnordost.streetcomplete.osm.isForwardOneway
 import de.westnordost.streetcomplete.osm.isNotOnewayForCyclists
 import de.westnordost.streetcomplete.osm.isReversedOneway
 import de.westnordost.streetcomplete.util.ktx.containsAny
 
-data class LeftAndRightCycleway(val left: Cycleway?, val right: Cycleway?)
-
 /** Returns the Cycleway values for the left and right side using the given tags */
 fun createCyclewaySides(tags: Map<String, String>, isLeftHandTraffic: Boolean): LeftAndRightCycleway? {
-    if (!tags.keys.containsAny(KNOWN_CYCLEWAY_KEYS)) return null
+    if (!tags.keys.containsAny(KNOWN_CYCLEWAY_AND_RELATED_KEYS)) return null
 
     val isForwardOneway = isForwardOneway(tags)
     val isReversedOneway = isReversedOneway(tags)
@@ -39,11 +20,14 @@ fun createCyclewaySides(tags: Map<String, String>, isLeftHandTraffic: Boolean):
     val isOnewayButNotForCyclists = isOneway && isNotOnewayForCyclists(tags, isLeftHandTraffic)
 
     // opposite tagging implies a oneway. So tagging is not understood if tags seem to contradict each other
-    val isAnyOppositeTagging = tags.filterKeys { it in KNOWN_CYCLEWAY_KEYS }.values.any { it.startsWith("opposite") }
+    val isAnyOppositeTagging = tags.filterKeys { it in KNOWN_CYCLEWAY_AND_RELATED_KEYS }.values.any { it.startsWith("opposite") }
     if (!isOneway && isAnyOppositeTagging) return null
 
-    var left: Cycleway?
-    var right: Cycleway?
+    var left: Cycleway? = null
+    var right: Cycleway? = null
+
+    // first expand cycleway:both etc into cycleway:left + cycleway:right etc
+    val expandedTags = expandRelevantSidesTags(tags)
 
     /* For oneways, the naked "cycleway"-keys should be interpreted differently:
     *  E.g. a cycleway=lane in a oneway=yes probably means that only in the flow direction, there
@@ -51,126 +35,138 @@ fun createCyclewaySides(tags: Map<String, String>, isLeftHandTraffic: Boolean):
     *  direction.
     *  Whether there is anything each in the other direction, is not defined, so we have to treat
     *  it that way. */
-    val cycleway = createCyclewayForSide(tags, null, isReverseSideRight)
-    if (isOneway && cycleway != null && cycleway != NONE) {
-        if (isOpposite) {
-            if (isReverseSideRight) {
-                left = null
-                right = cycleway
-            } else {
-                left = cycleway
-                right = null
-            }
-        } else {
-            if (isReverseSideRight) {
-                left = cycleway
-                right = null
-            } else {
-                left = null
-                right = cycleway
-            }
-        }
+    val nakedCycleway = createCyclewayForSide(expandedTags, null, isLeftHandTraffic)
+    if (isOneway && nakedCycleway != null && nakedCycleway != NONE) {
+        val isRight = if (isOpposite) isReverseSideRight else !isReverseSideRight
+        if (isRight) right = nakedCycleway else left = nakedCycleway
     } else {
-        // first expand cycleway:both etc into cycleway:left + cycleway:right etc
-        val expandedTags = expandRelevantSidesTags(tags)
-        // then get the values for left and right
-        left = createCyclewayForSide(expandedTags, "left", isReverseSideRight)
-        right = createCyclewayForSide(expandedTags, "right", isReverseSideRight)
+        left = createCyclewayForSide(expandedTags, false, isLeftHandTraffic)
+        right = createCyclewayForSide(expandedTags, true, isLeftHandTraffic)
     }
 
+    val leftDir = createDirectionForSide(expandedTags, false, isLeftHandTraffic)
+    val rightDir = createDirectionForSide(expandedTags, true, isLeftHandTraffic)
+
     /* if there is no cycleway in a direction but it is a oneway in the other direction but not
        for cyclists, we have a special selection for that */
     if (isOnewayButNotForCyclists) {
-        if ((left == NONE || left == null) && !isReverseSideRight) left = NONE_NO_ONEWAY
-        if ((right == NONE || right == null) && isReverseSideRight) right = NONE_NO_ONEWAY
+        if ((left == NONE || left == null) && !isReverseSideRight && rightDir != Direction.BOTH) {
+            left = NONE_NO_ONEWAY
+        }
+        if ((right == NONE || right == null) && isReverseSideRight && leftDir != Direction.BOTH) {
+            right = NONE_NO_ONEWAY
+        }
     }
 
     if (left == null && right == null) return null
 
-    return LeftAndRightCycleway(left, right)
+    return LeftAndRightCycleway(
+        left?.let { CyclewayAndDirection(it, leftDir) },
+        right?.let { CyclewayAndDirection(it, rightDir) }
+    )
 }
 
 /** Returns the Cycleway value using the given tags, for the given side (left or right).
  *  Returns null if nothing (understood) is tagged */
 private fun createCyclewayForSide(
     tags: Map<String, String>,
-    side: String?,
-    isReverseSideRight: Boolean
+    isRight: Boolean?,
+    isLeftHandTraffic: Boolean
 ): Cycleway? {
-
-    val sideVal = if (side != null) ":$side" else ""
-    val cyclewayKey = "cycleway$sideVal"
-
-    val oneway = tags["$cyclewayKey:oneway"]
-
-    val isOnewayInUnexpectedDirection = when (side) {
-        "left" -> when (oneway) {
-            "yes" -> !isReverseSideRight
-            "-1" -> isReverseSideRight
-            else -> false
-        }
-        "right" -> when (oneway) {
-            "yes" -> isReverseSideRight
-            "-1" -> !isReverseSideRight
-            else -> false
-        }
-        else -> false
+    val sideVal = when (isRight) {
+        true -> ":right"
+        false -> ":left"
+        null -> ""
     }
-    if (isOnewayInUnexpectedDirection) return UNKNOWN
+    val cyclewayKey = "cycleway$sideVal"
 
     val cycleway = tags[cyclewayKey]
-    val isDual = oneway == "no"
     val cyclewayLane = tags["$cyclewayKey:lane"]
     val isSegregated = tags["$cyclewayKey:segregated"] != "no"
 
     val result = when (cycleway) {
         "lane", "opposite_lane" -> {
             when (cyclewayLane) {
-                "exclusive", "exclusive_lane", "mandatory" -> {
-                    if (isDual) DUAL_LANE
-                    else        EXCLUSIVE_LANE
-                }
-                null -> {
-                    if (isDual) DUAL_LANE
-                    else        UNSPECIFIED_LANE
-                }
-                "advisory", "advisory_lane", "soft_lane", "dashed" -> ADVISORY_LANE
-                else                                               -> UNKNOWN_LANE
+                "exclusive" -> EXCLUSIVE_LANE
+                null ->        UNSPECIFIED_LANE
+                "advisory" ->  ADVISORY_LANE
+                "yes", "right", "left", "both", "shoulder", "soft_lane", "mandatory",
+                "advisory_lane", "exclusive_lane" -> INVALID
+                "pictogram" -> INVALID
+                else        -> UNKNOWN_LANE
             }
         }
         "shared_lane" -> {
             when (cyclewayLane) {
-                "advisory", "advisory_lane", "soft_lane", "dashed" -> SUGGESTION_LANE
-                "pictogram"                                        -> PICTOGRAMS
-                null                                               -> UNSPECIFIED_SHARED_LANE
-                else                                               -> UNKNOWN_SHARED_LANE
+                "advisory"  -> SUGGESTION_LANE
+                "pictogram" -> PICTOGRAMS
+                null        -> UNSPECIFIED_SHARED_LANE
+                "yes", "right", "left", "both", "shoulder", "soft_lane", "mandatory",
+                "advisory_lane", "exclusive_lane" -> INVALID
+                "exclusive" -> INVALID
+                else        -> UNKNOWN_SHARED_LANE
             }
         }
         "track", "opposite_track" -> {
-            when {
-                !isSegregated -> SIDEWALK_EXPLICIT
-                isDual        -> DUAL_TRACK
-                else          -> TRACK
-            }
+            if (isSegregated) TRACK else SIDEWALK_EXPLICIT
         }
         "separate" -> SEPARATE
-        "no", "none", "opposite" -> NONE
+        "no", "opposite" -> NONE
         "share_busway", "opposite_share_busway" -> BUSWAY
-        "yes", "right", "left", "both", "shared" -> INVALID
+        "shoulder" -> SHOULDER
+        // values known to be invalid, ambiguous or obsolete:
+        // 1.2% - ambiguous: there are more precise tags
+        "yes", "right", "left", "both",
+        "on_street", "segregated", "shared", // segregated from, shared with what?
+        "sidewalk", "share_sidewalk", // allowed on sidewalk or mandatory on sidewalk?
+        "unmarked_lane", // I don't even...
+        // 0.4% - invalid: maybe synonymous(?) to valid tag or tag combination but never documented
+        "none", // no
+        "sidepath", "use_sidepath", // separate?
+        "buffered_lane", "buffered", "soft_lane", "doorzone", // lane + subtags?
+         // 0.1% - troll tags
+        "proposed", "construction",
+            -> INVALID
         null -> null
         else -> UNKNOWN
     }
 
+    // fall back to bicycle=use_sidepath if set because it implies there is a separate cycleway
+    if (result == null) {
+        if (isRight != null) {
+            val direction = if (isLeftHandTraffic xor isRight) "forward" else "backward"
+            if (tags["bicycle:$direction"] == "use_sidepath") return SEPARATE
+        }
+        if (tags["bicycle"] == "use_sidepath") return SEPARATE
+    }
     return result
 }
 
+private fun createDirectionForSide(
+    tags: Map<String, String>,
+    isRight: Boolean,
+    isLeftHandTraffic: Boolean
+): Direction {
+    val sideVal = if (isRight) ":right" else ":left"
+    val cyclewayKey = "cycleway$sideVal"
+    val explicitDirection = when (tags["$cyclewayKey:oneway"]) {
+        "yes" -> Direction.FORWARD
+        "-1" ->  Direction.BACKWARD
+        "no" ->  Direction.BOTH
+        else ->  null
+    }
+    val defaultDirection =
+        if (isRight xor isLeftHandTraffic) Direction.FORWARD else Direction.BACKWARD
+
+    return explicitDirection ?: defaultDirection
+}
+
 private fun expandRelevantSidesTags(tags: Map<String, String>): Map<String, String> {
     val result = tags.toMutableMap()
     expandSidesTag("cycleway", "", result)
     expandSidesTag("cycleway", "lane", result)
     expandSidesTag("cycleway", "oneway", result)
     expandSidesTag("cycleway", "segregated", result)
-    expandSidesTag("sidewalk", "bicycle", result)
     return result
 }
 
@@ -185,6 +181,7 @@ private fun expandSidesTag(keyPrefix: String, keyPostfix: String, tags: MutableM
     }
 }
 
-private val KNOWN_CYCLEWAY_KEYS = listOf(
-    "cycleway", "cycleway:left", "cycleway:right", "cycleway:both"
+private val KNOWN_CYCLEWAY_AND_RELATED_KEYS = listOf(
+    "cycleway", "cycleway:left", "cycleway:right", "cycleway:both",
+    "bicycle", "bicycle:forward", "bicycle:backward"
 )
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCycleway.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCycleway.kt
new file mode 100644
index 00000000000..6323a10191d
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCycleway.kt
@@ -0,0 +1,20 @@
+package de.westnordost.streetcomplete.osm.cycleway_separate
+
+enum class SeparateCycleway {
+    /** No bicycles here */
+    NONE,
+    /** Bicycles allowed on footway / path */
+    ALLOWED,
+    /** Unspecific value: This footway / path is not designated for cyclists. I.e. it is
+     *  either NONE or ALLOWED. This value is never output by the parser. */
+    NON_DESIGNATED,
+    /** Designated but not segregated from footway mapped on same way */
+    NON_SEGREGATED,
+    /** Designated and segregated from footway mapped on same way */
+    SEGREGATED,
+    /** This way is a cycleway only, no footway mapped on the same way */
+    EXCLUSIVE,
+    /** This way is a cycleway only, however it has a sidewalk mapped on the same way, like some
+     *  sort of tiny road for cyclists only */
+    EXCLUSIVE_WITH_SIDEWALK
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayCreator.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayCreator.kt
new file mode 100644
index 00000000000..82ad030d3fe
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayCreator.kt
@@ -0,0 +1,85 @@
+package de.westnordost.streetcomplete.osm.cycleway_separate
+
+import de.westnordost.streetcomplete.osm.cycleway_separate.SeparateCycleway.*
+import de.westnordost.streetcomplete.osm.Tags
+import de.westnordost.streetcomplete.osm.hasCheckDateForKey
+import de.westnordost.streetcomplete.osm.sidewalk.KNOWN_SIDEWALK_KEYS
+import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk
+import de.westnordost.streetcomplete.osm.sidewalk.any
+import de.westnordost.streetcomplete.osm.sidewalk.createSidewalkSides
+import de.westnordost.streetcomplete.osm.updateCheckDateForKey
+
+fun SeparateCycleway.applyTo(tags: Tags) {
+    val isCycleway = tags["highway"] == "cycleway"
+
+    // tag bicycle=*, foot=* and retag highway=* if necessary
+    when (this) {
+        NONE, ALLOWED, NON_DESIGNATED -> {
+            // not a cycleway if not designated as one!
+            if (isCycleway) {
+                tags["highway"] = if (tags["foot"]  == "designated") "footway" else "path"
+            }
+
+            when (this) {
+                NONE -> tags["bicycle"] = "no"
+                ALLOWED -> tags["bicycle"] = "yes"
+                else -> if (tags["bicycle"] == "designated") tags.remove("bicycle")
+            }
+            if (tags.containsKey("foot") && tags["foot"] !in listOf("yes", "designated")) {
+                tags["foot"] = "yes"
+            }
+        }
+        NON_SEGREGATED, SEGREGATED -> {
+            if (!isCycleway || tags.containsKey("bicycle")) {
+                tags["bicycle"] = "designated"
+            }
+            if (isCycleway || tags.containsKey("foot")) {
+                tags["foot"] = "designated"
+            }
+        }
+        EXCLUSIVE, EXCLUSIVE_WITH_SIDEWALK -> {
+            // retag if it is an exclusive cycleway
+            tags["highway"] = "cycleway"
+            if (tags.containsKey("bicycle")) tags["bicycle"] = "designated"
+
+            if (this == EXCLUSIVE) tags["foot"] = "no"
+            // follow the same pattern as for roads here: It is uncommon for roads to have foot
+            // tagged at all when such roads have sidewalks
+            else tags.remove("foot")
+        }
+    }
+
+    // tag or remove sidewalk
+    val hasSidewalk = createSidewalkSides(tags)?.any { it == Sidewalk.YES } == true || tags["sidewalk"] == "yes"
+    when (this) {
+        EXCLUSIVE_WITH_SIDEWALK -> {
+            if (!hasSidewalk) {
+                KNOWN_SIDEWALK_KEYS.forEach { tags.remove(it) }
+                tags["sidewalk"] = "yes" // cannot be more specific here
+            }
+        }
+        else -> {
+            if (hasSidewalk) {
+                KNOWN_SIDEWALK_KEYS.forEach { tags.remove(it) }
+            }
+        }
+    }
+
+    // tag segregated
+    when (this) {
+        NONE, ALLOWED, NON_DESIGNATED, EXCLUSIVE, EXCLUSIVE_WITH_SIDEWALK -> {
+            tags.remove("segregated")
+        }
+        NON_SEGREGATED -> {
+            tags["segregated"] = "no"
+        }
+        SEGREGATED -> {
+            tags["segregated"] = "yes"
+        }
+    }
+
+    // update check date
+    if (!tags.hasChanges || tags.hasCheckDateForKey("bicycle")) {
+        tags.updateCheckDateForKey("bicycle")
+    }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayItem.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayItem.kt
new file mode 100644
index 00000000000..02c441ba32e
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayItem.kt
@@ -0,0 +1,30 @@
+package de.westnordost.streetcomplete.osm.cycleway_separate
+
+import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.osm.cycleway_separate.SeparateCycleway.*
+import de.westnordost.streetcomplete.view.image_select.Item
+
+fun SeparateCycleway.asItem(isLeftHandTraffic: Boolean) =
+    Item(this, getIconResId(isLeftHandTraffic), titleResId)
+
+private val SeparateCycleway.titleResId: Int get() = when (this) {
+    NONE ->           R.string.separate_cycleway_no
+    ALLOWED ->        R.string.separate_cycleway_allowed
+    NON_DESIGNATED -> R.string.separate_cycleway_no_or_allowed
+    NON_SEGREGATED -> R.string.separate_cycleway_non_segregated
+    SEGREGATED ->     R.string.separate_cycleway_segregated
+    EXCLUSIVE ->      R.string.separate_cycleway_exclusive
+    EXCLUSIVE_WITH_SIDEWALK ->  R.string.separate_cycleway_with_sidewalk
+}
+
+private fun SeparateCycleway.getIconResId(isLeftHandTraffic: Boolean): Int = when (this) {
+    NONE ->           R.drawable.ic_separate_cycleway_no
+    ALLOWED ->        R.drawable.ic_separate_cycleway_allowed
+    NON_DESIGNATED -> R.drawable.ic_separate_cycleway_no
+    NON_SEGREGATED -> R.drawable.ic_separate_cycleway_not_segregated
+    SEGREGATED ->     if (isLeftHandTraffic) R.drawable.ic_separate_cycleway_segregated_l
+                      else                   R.drawable.ic_separate_cycleway_segregated
+    EXCLUSIVE ->      R.drawable.ic_separate_cycleway_exclusive
+    EXCLUSIVE_WITH_SIDEWALK ->  if (isLeftHandTraffic) R.drawable.ic_separate_cycleway_with_sidewalk_l
+                      else                   R.drawable.ic_separate_cycleway_with_sidewalk
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayParser.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayParser.kt
new file mode 100644
index 00000000000..218fa4f9fa6
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayParser.kt
@@ -0,0 +1,40 @@
+package de.westnordost.streetcomplete.osm.cycleway_separate
+
+import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk
+import de.westnordost.streetcomplete.osm.sidewalk.any
+import de.westnordost.streetcomplete.osm.sidewalk.createSidewalkSides
+
+/** Returns the situation for a separately mapped cycleway */
+fun createSeparateCycleway(tags: Map<String, String>): SeparateCycleway? {
+
+    /* bridleways (with cycleways) are not supported, because the complexity explodes when a path
+       can not only have have states ranging between two tags with values "no, yes, designated,
+       segregated from each other, with sidewalk" but three, with any mixture between these
+       possible.
+       In particular, it is not clear what "segregated" refers to on a path that potentially allows
+       access to all three modes of travel and when re-tagging e.g. an exclusive cycleway to a
+       non-designated one, it is unclear whether foot=yes or horse=yes or both should be added. So,
+       this data model can only be about the relation between foot & bicycle, not about all three.
+     */
+    if (tags["highway"] !in listOf("path", "footway", "cycleway")) return null
+
+    // cycleway implies bicycle=designated
+    val bicycle = tags["bicycle"] ?: if (tags["highway"] == "cycleway") "designated" else null
+    if (bicycle != "designated") return if (bicycle == "yes") SeparateCycleway.ALLOWED else SeparateCycleway.NONE
+
+    val hasSidewalk = createSidewalkSides(tags)?.any { it == Sidewalk.YES } == true || tags["sidewalk"] == "yes"
+    if (hasSidewalk) {
+        return SeparateCycleway.EXCLUSIVE_WITH_SIDEWALK
+    }
+
+    // footway implies foot=designated, path implies foot=yes
+    val foot = tags["foot"] ?: when (tags["highway"]) {
+        "footway" -> "designated"
+        "path" -> "yes"
+        else -> null
+    }
+    if (foot !in listOf("yes", "designated")) return SeparateCycleway.EXCLUSIVE
+
+    val segregated = tags["segregated"] == "yes"
+    return if (segregated) SeparateCycleway.SEGREGATED else SeparateCycleway.NON_SEGREGATED
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/shoulders/ShouldersParser.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/shoulders/ShouldersParser.kt
index a66cf4f08b7..a2ad2081973 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/shoulders/ShouldersParser.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/shoulders/ShouldersParser.kt
@@ -1,5 +1,8 @@
 package de.westnordost.streetcomplete.osm.shoulders
 
+import de.westnordost.streetcomplete.osm.isOneway
+import de.westnordost.streetcomplete.osm.isReversedOneway
+
 /** Returns on which sides are shoulders. Returns null if there is no shoulders tagging at all */
 fun createShoulders(tags: Map<String, String>, isLeftHandTraffic: Boolean): Shoulders? {
     val shoulder = createShouldersDefault(tags, isLeftHandTraffic)
@@ -22,11 +25,9 @@ private fun createShouldersDefault(tags: Map<String, String>, isLeftHandTraffic:
     "right" -> Shoulders(left = false, right = true)
     "both" -> Shoulders(left = true, right = true)
     "yes" -> {
-        val isForwardOneway = tags["oneway"] == "yes" || (tags["junction"] == "roundabout" && tags["oneway"] != "-1")
-        val isReversedOneway = tags["oneway"] == "-1"
-        val isOneway = isReversedOneway || isForwardOneway
+        val isReversedOneway = isReversedOneway(tags)
         val isReverseSideRight = isReversedOneway xor isLeftHandTraffic
-        if (isOneway) {
+        if (isOneway(tags)) {
             Shoulders(left = isReverseSideRight, right = !isReverseSideRight)
         } else {
             Shoulders(left = true, right = true)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/Sidewalk.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/Sidewalk.kt
index e6db05db36b..e021fe803d1 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/Sidewalk.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/Sidewalk.kt
@@ -1,74 +1,20 @@
 package de.westnordost.streetcomplete.osm.sidewalk
 
-import de.westnordost.streetcomplete.osm.Tags
-import de.westnordost.streetcomplete.osm.hasCheckDateForKey
 import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.*
-import de.westnordost.streetcomplete.osm.updateCheckDateForKey
 
 data class LeftAndRightSidewalk(val left: Sidewalk?, val right: Sidewalk?)
 
-enum class Sidewalk {
-    YES,
-    NO,
-    SEPARATE,
-    INVALID
-}
+fun LeftAndRightSidewalk.any(block: (sidewalk: Sidewalk?) -> Boolean): Boolean =
+    block(left) || block(right)
 
 fun LeftAndRightSidewalk.validOrNullValues(): LeftAndRightSidewalk {
     if (left != INVALID && right != INVALID) return this
     return LeftAndRightSidewalk(left?.takeIf { it != INVALID }, right?.takeIf { it != INVALID })
 }
 
-fun LeftAndRightSidewalk.applyTo(tags: Tags) {
-    val currentSidewalk = createSidewalkSides(tags)
-
-    // was set before and changed: may be incorrect now - remove subtags!
-    if (currentSidewalk?.left != null && currentSidewalk.left != left ||
-        currentSidewalk?.right != null && currentSidewalk.right != right) {
-        val sidewalkSubtagging = Regex("^sidewalk:(left|right|both):.*")
-        for (key in tags.keys) {
-            if (key.matches(sidewalkSubtagging)) {
-                tags.remove(key)
-            }
-        }
-    }
-
-    val sidewalkValue = simpleOsmValue
-    if (sidewalkValue != null) {
-        tags["sidewalk"] = sidewalkValue
-        // In case of previous incomplete sidewalk tagging
-        tags.remove("sidewalk:left")
-        tags.remove("sidewalk:right")
-        tags.remove("sidewalk:both")
-    } else {
-        if (left != null)  tags["sidewalk:left"] = left.osmValue
-        if (right != null) tags["sidewalk:right"] = right.osmValue
-        // In case of previous incorrect sidewalk tagging
-        tags.remove("sidewalk:both")
-        tags.remove("sidewalk")
-    }
-
-    if (!tags.hasChanges || tags.hasCheckDateForKey("sidewalk")) {
-        tags.updateCheckDateForKey("sidewalk")
-    }
-}
-
-/** Value for the sidewalk=* key. Returns null for combinations that can't be expressed with the
- *  sidewalk=* key. */
-private val LeftAndRightSidewalk.simpleOsmValue: String? get() = when {
-    left == YES && right == YES -> "both"
-    left == YES && right == NO ->  "left"
-    left == NO && right == YES ->  "right"
-    left == NO && right == NO ->   "no"
-    left == SEPARATE && right == SEPARATE -> "separate"
-    else -> null
-}
-
-private val Sidewalk.osmValue: String get() = when (this) {
-    YES -> "yes"
-    NO -> "no"
-    SEPARATE -> "separate"
-    else -> {
-        throw IllegalArgumentException("Attempting to tag invalid sidewalk")
-    }
+enum class Sidewalk {
+    YES,
+    NO,
+    SEPARATE,
+    INVALID
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkCreator.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkCreator.kt
new file mode 100644
index 00000000000..49bc9672816
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkCreator.kt
@@ -0,0 +1,92 @@
+package de.westnordost.streetcomplete.osm.sidewalk
+
+import de.westnordost.streetcomplete.osm.Tags
+import de.westnordost.streetcomplete.osm.expandSides
+import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.*
+import de.westnordost.streetcomplete.osm.hasCheckDateForKey
+import de.westnordost.streetcomplete.osm.mergeSides
+import de.westnordost.streetcomplete.osm.updateCheckDateForKey
+
+fun LeftAndRightSidewalk.applyTo(tags: Tags) {
+    if (left == null && right == null) return
+    /* for being able to modify only one side (e.g. `left` is null while `right` is not null),
+       first the conflated and merged sidewalk values (sidewalk=both and sidewalk:both=yes etc.)
+       need to be separated.
+       So even if there is an invalid value such as sidewalk=narrow but only the right side
+       is modified to "yes" while the left side is not touched, it means that in the end, the
+       invalid value must still be in the end result, like this:
+       - sidewalk:left=narrow
+       - sidewalk:right=yes
+       First separating the values and then later conflating them again, if possible, solves this.
+    */
+
+    /* NOT expanding the bare tag, because the bare tag follows a different schema
+       E.g. sidewalk=both  !=  sidewalk:left=both + sidewalk:right=both
+       See separateConflatedSidewalk for proper parsing of that */
+    tags.expandSides("sidewalk", includeBareTag = false)
+    tags.separateConflatedSidewalk()
+
+    if (left != null)  tags["sidewalk:left"] = left.osmValue
+    if (right != null) tags["sidewalk:right"] = right.osmValue
+
+    // use shortcut syntax if possible, preferred by community according to usage numbers on taginfo
+    tags.conflateSidewalk()
+    tags.mergeSides("sidewalk")
+
+    if (!tags.hasChanges || tags.hasCheckDateForKey("sidewalk")) {
+        tags.updateCheckDateForKey("sidewalk")
+    }
+}
+
+/** converts sidewalk=both to sidewalk:left=yes + sidewalk:right=yes etc. */
+private fun Tags.separateConflatedSidewalk() {
+    val leftRight = getSeparatedSidewalkValues(get("sidewalk")) ?: return
+    // don't overwrite more specific values if already set
+    if (!containsKey("sidewalk:left")) set("sidewalk:left", leftRight.first)
+    if (!containsKey("sidewalk:right")) set("sidewalk:right", leftRight.second)
+    remove("sidewalk")
+}
+
+private fun getSeparatedSidewalkValues(sidewalk: String?): Pair<String, String>? = when (sidewalk) {
+    "both" ->  Pair("yes", "yes")
+    "left" ->  Pair("yes", "no")
+    "right" -> Pair("no", "yes")
+    null ->    null
+    // for "separate", "no", etc., and also invalid values
+    else ->    Pair(sidewalk, sidewalk)
+}
+
+/** converts sidewalk:left=yes + sidewalk:right=yes to sidewalk=both etc. */
+private fun Tags.conflateSidewalk() {
+    val sidewalk = getConflatedSidewalkValue(get("sidewalk:left"), get("sidewalk:right"))
+    if (sidewalk != null) {
+        remove("sidewalk:left")
+        remove("sidewalk:right")
+        set("sidewalk", sidewalk)
+    }
+}
+
+private fun getConflatedSidewalkValue(left: String?, right: String?): String? = when {
+    left == "yes" && right == "no" -> "left"
+    left == "no" && right == "yes" -> "right"
+    /* shall only work for "yes" and "no" because the wiki states that sidewalk:both=separate
+       should be preferred over sidewalk=separate: The former is more explicit while for the latter,
+       it is not entirely clear if it is meant for both sides or only one side.
+       (Same as with cycleway=separate vs cycleway:both=separate) */
+    left == right -> when (left) {
+        "yes" -> "both"
+        "no" ->  "no"
+        else ->  null
+    }
+    else -> null
+}
+
+private val Sidewalk.osmValue: String get() = when (this) {
+    YES -> "yes"
+    NO -> "no"
+    SEPARATE -> "separate"
+    else -> {
+        throw IllegalArgumentException("Attempting to tag invalid sidewalk")
+    }
+}
+
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkParser.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkParser.kt
index 6443e4eae95..54cb1b18c9f 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkParser.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkParser.kt
@@ -64,6 +64,6 @@ private fun createSidewalkSide(tag: String?): Sidewalk? = when (tag) {
     else -> INVALID
 }
 
-private val KNOWN_SIDEWALK_KEYS = listOf(
+val KNOWN_SIDEWALK_KEYS = listOf(
     "sidewalk", "sidewalk:left", "sidewalk:right", "sidewalk:both"
 )
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/StreetParking.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/StreetParking.kt
index 1171083ae77..73e1d864212 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/StreetParking.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/StreetParking.kt
@@ -1,16 +1,11 @@
 package de.westnordost.streetcomplete.osm.street_parking
 
-import de.westnordost.streetcomplete.osm.Tags
-import de.westnordost.streetcomplete.osm.hasCheckDateForKey
 import de.westnordost.streetcomplete.osm.street_parking.ParkingOrientation.DIAGONAL
 import de.westnordost.streetcomplete.osm.street_parking.ParkingOrientation.PARALLEL
 import de.westnordost.streetcomplete.osm.street_parking.ParkingOrientation.PERPENDICULAR
 import de.westnordost.streetcomplete.osm.street_parking.ParkingPosition.HALF_ON_KERB
 import de.westnordost.streetcomplete.osm.street_parking.ParkingPosition.ON_KERB
 import de.westnordost.streetcomplete.osm.street_parking.ParkingPosition.ON_STREET
-import de.westnordost.streetcomplete.osm.street_parking.ParkingPosition.PAINTED_AREA_ONLY
-import de.westnordost.streetcomplete.osm.street_parking.ParkingPosition.STREET_SIDE
-import de.westnordost.streetcomplete.osm.updateCheckDateForKey
 import kotlinx.serialization.Serializable
 
 data class LeftAndRightStreetParking(val left: StreetParking?, val right: StreetParking?)
@@ -74,64 +69,3 @@ private val ParkingPosition.estimatedWidthOnRoadFactor: Float get() = when (this
     ON_KERB -> 0f
     else -> 0.5f // otherwise let's assume it is somehow on the street
 }
-
-fun LeftAndRightStreetParking.applyTo(tags: Tags) {
-    val currentParking = createStreetParkingSides(tags)
-
-    // first clear previous
-    val keyToRemove = Regex("parking:lane:(both|left|right)(:(parallel|diagonal|perpendicular))?")
-    for (key in tags.keys) {
-        if (key.matches(keyToRemove)) tags.remove(key)
-    }
-
-    val r = right ?: currentParking?.right
-    val l = left ?: currentParking?.left
-
-    // parking:lane:<left/right/both>
-    val laneRight = r?.toOsmLaneValue()
-    val laneLeft =  l?.toOsmLaneValue()
-
-    if (laneLeft == laneRight) {
-        if (laneLeft != null) tags["parking:lane:both"] = laneLeft
-    } else {
-        if (laneLeft != null) tags["parking:lane:left"] = laneLeft
-        if (laneRight != null) tags["parking:lane:right"] = laneRight
-    }
-
-    // parking:lane:<left/right/both>:<parallel/diagonal/perpendicular>
-    val positionRight = (r as? StreetParkingPositionAndOrientation)?.position?.toOsmValue()
-    val positionLeft = (l as? StreetParkingPositionAndOrientation)?.position?.toOsmValue()
-
-    if (laneLeft == laneRight && positionLeft == positionRight) {
-        if (positionLeft != null) tags["parking:lane:both:$laneLeft"] = positionLeft
-    } else {
-        if (positionLeft != null) tags["parking:lane:left:$laneLeft"] = positionLeft
-        if (positionRight != null) tags["parking:lane:right:$laneRight"] = positionRight
-    }
-
-    if (!tags.hasChanges || tags.hasCheckDateForKey("parking:lane")) {
-        tags.updateCheckDateForKey("parking:lane")
-    }
-}
-
-/** get the OSM value for the parking:lane key */
-private fun StreetParking.toOsmLaneValue(): String = when (this) {
-    is StreetParkingPositionAndOrientation -> orientation.toOsmValue()
-    NoStreetParking -> "no"
-    StreetParkingSeparate -> "separate"
-    UnknownStreetParking, IncompleteStreetParking -> throw IllegalArgumentException("Attempting to tag invalid parking lane")
-}
-
-private fun ParkingPosition.toOsmValue() = when (this) {
-    ON_STREET -> "on_street"
-    HALF_ON_KERB -> "half_on_kerb"
-    ON_KERB -> "on_kerb"
-    STREET_SIDE -> "street_side"
-    PAINTED_AREA_ONLY -> "painted_area_only"
-}
-
-private fun ParkingOrientation.toOsmValue() = when (this) {
-    PARALLEL -> "parallel"
-    DIAGONAL -> "diagonal"
-    PERPENDICULAR -> "perpendicular"
-}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingCreator.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingCreator.kt
new file mode 100644
index 00000000000..87d1a9a34d5
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingCreator.kt
@@ -0,0 +1,62 @@
+package de.westnordost.streetcomplete.osm.street_parking
+
+import de.westnordost.streetcomplete.osm.Tags
+import de.westnordost.streetcomplete.osm.expandSides
+import de.westnordost.streetcomplete.osm.hasCheckDateForKey
+import de.westnordost.streetcomplete.osm.mergeSides
+import de.westnordost.streetcomplete.osm.updateCheckDateForKey
+
+fun LeftAndRightStreetParking.applyTo(tags: Tags) {
+    if (left == null && right == null) return
+    /* for being able to modify only one side (e.g. `left` is null while `right` is not null),
+       the sides conflated in `:both` keys need to be separated first. E.g. `parking:lane=no`
+       when left side is made `separate´ should become
+       - parking:lane:right=no
+       - parking:lane:left=separate
+       First separating the values and then later conflating them again, if possible, solves this.
+     */
+    tags.expandSides("parking:lane")
+    ParkingOrientation.values().forEach { tags.expandSides("parking:lane", it.osmValue, true) }
+
+    // parking:lane:<left/right>
+    left?.applyTo(tags, "left")
+    right?.applyTo(tags, "right")
+
+    tags.mergeSides("parking:lane")
+    ParkingOrientation.values().forEach { tags.mergeSides("parking:lane", it.osmValue) }
+
+    if (!tags.hasChanges || tags.hasCheckDateForKey("parking:lane")) {
+        tags.updateCheckDateForKey("parking:lane")
+    }
+}
+
+private fun StreetParking.applyTo(tags: Tags, side: String) {
+    tags["parking:lane:$side"] = osmLaneValue
+    // clear previous orientation(s)
+    ParkingOrientation.values().forEach { tags.remove("parking:lane:$side:${it.osmValue}") }
+    if (this is StreetParkingPositionAndOrientation) {
+        tags["parking:lane:$side:$osmLaneValue"] = position.osmValue
+    }
+}
+
+/** get the OSM value for the parking:lane key */
+private val StreetParking.osmLaneValue get() = when (this) {
+    is StreetParkingPositionAndOrientation -> orientation.osmValue
+    NoStreetParking -> "no"
+    StreetParkingSeparate -> "separate"
+    UnknownStreetParking, IncompleteStreetParking -> throw IllegalArgumentException("Attempting to tag invalid parking lane")
+}
+
+private val ParkingPosition.osmValue get() = when (this) {
+    ParkingPosition.ON_STREET ->         "on_street"
+    ParkingPosition.HALF_ON_KERB ->      "half_on_kerb"
+    ParkingPosition.ON_KERB ->           "on_kerb"
+    ParkingPosition.STREET_SIDE ->       "street_side"
+    ParkingPosition.PAINTED_AREA_ONLY -> "painted_area_only"
+}
+
+private val ParkingOrientation.osmValue get() = when (this) {
+    ParkingOrientation.PARALLEL ->      "parallel"
+    ParkingOrientation.DIAGONAL ->      "diagonal"
+    ParkingOrientation.PERPENDICULAR -> "perpendicular"
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingParser.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingParser.kt
index e459ccc745e..c226f3a1b4e 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingParser.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingParser.kt
@@ -67,7 +67,6 @@ private fun String.toParkingPosition() = when (this) {
 private fun expandRelevantSidesTags(tags: Map<String, String>): Map<String, String> {
     val result = tags.toMutableMap()
     expandSidesTag("parking:lane", "", result)
-    expandSidesTag("parking:condition", "", result)
     expandSidesTag("parking:lane", "parallel", result)
     expandSidesTag("parking:lane", "diagonal", result)
     expandSidesTag("parking:lane", "perpendicular", result)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/WithFootnoteDrawable.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/WithFootnoteDrawable.kt
deleted file mode 100644
index accc75ddb8e..00000000000
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/street_parking/WithFootnoteDrawable.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package de.westnordost.streetcomplete.osm.street_parking
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.drawable.Drawable
-import de.westnordost.streetcomplete.R
-import de.westnordost.streetcomplete.view.DrawableWrapper
-
-/** Container that contains another drawable but adds a fat "*" in the upper right corner */
-class WithFootnoteDrawable(context: Context, drawable: Drawable) : DrawableWrapper(drawable) {
-
-    private val footnoteDrawable = context.getDrawable(R.drawable.ic_fat_footnote)!!
-
-    override fun draw(canvas: Canvas) {
-        drawable.bounds = bounds
-        drawable.draw(canvas)
-        val size = bounds.width() * 3 / 8
-        footnoteDrawable.setBounds(bounds.width() - size, 0, bounds.width(), size)
-        footnoteDrawable.draw(canvas)
-    }
-}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/AbstractOverlayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/AbstractOverlayForm.kt
index 56d4898ebcc..d353ff74347 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/overlays/AbstractOverlayForm.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/AbstractOverlayForm.kt
@@ -40,7 +40,6 @@ import de.westnordost.streetcomplete.data.osm.mapdata.Node
 import de.westnordost.streetcomplete.data.osm.mapdata.Way
 import de.westnordost.streetcomplete.data.overlays.OverlayRegistry
 import de.westnordost.streetcomplete.databinding.FragmentOverlayBinding
-import de.westnordost.streetcomplete.quests.AnswerItem
 import de.westnordost.streetcomplete.screens.main.bottom_sheet.IsCloseableBottomSheet
 import de.westnordost.streetcomplete.screens.main.bottom_sheet.IsMapOrientationAware
 import de.westnordost.streetcomplete.screens.main.checkIsSurvey
@@ -53,7 +52,11 @@ import de.westnordost.streetcomplete.util.ktx.popOut
 import de.westnordost.streetcomplete.util.ktx.setMargins
 import de.westnordost.streetcomplete.util.ktx.toast
 import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope
+import de.westnordost.streetcomplete.view.CharSequenceText
+import de.westnordost.streetcomplete.view.ResText
 import de.westnordost.streetcomplete.view.RoundRectOutlineProvider
+import de.westnordost.streetcomplete.view.Text
+import de.westnordost.streetcomplete.view.add
 import de.westnordost.streetcomplete.view.insets_animation.respectSystemInsets
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -129,7 +132,7 @@ abstract class AbstractOverlayForm :
     // overridable by child classes
     open val contentLayoutResId: Int? = null
     open val contentPadding = true
-    open val otherAnswers = listOf<AnswerItem>()
+    open val otherAnswers = listOf<IAnswerItem>()
 
     interface Listener {
         /** The GPS position at which the user is displayed at */
@@ -185,6 +188,7 @@ abstract class AbstractOverlayForm :
         binding.speechbubbleContentContainer.outlineProvider = RoundRectOutlineProvider(
             cornerRadius, margin, margin, margin, margin
         )
+        binding.speechbubbleContentContainer.clipToOutline = true
 
         setTitleHintLabel(
             element?.tags?.let { getNameAndLocationLabel(it, resources, featureDictionary) }
@@ -333,7 +337,7 @@ abstract class AbstractOverlayForm :
         for (i in answers.indices) {
             val otherAnswer = answers[i]
             val order = answers.size - i
-            popup.menu.add(Menu.NONE, i, order, otherAnswer.titleResourceId)
+            popup.menu.add(Menu.NONE, i, order, otherAnswer.title)
         }
         popup.show()
 
@@ -343,8 +347,8 @@ abstract class AbstractOverlayForm :
         }
     }
 
-    private fun assembleOtherAnswers(): List<AnswerItem> {
-        val answers = mutableListOf<AnswerItem>()
+    private fun assembleOtherAnswers(): List<IAnswerItem> {
+        val answers = mutableListOf<IAnswerItem>()
 
         val element = element
         if (element != null) {
@@ -427,4 +431,16 @@ abstract class AbstractOverlayForm :
     }
 }
 
-data class AnswerItem(val titleResourceId: Int, val action: () -> Unit)
+
+interface IAnswerItem {
+    val title: Text
+    val action: () -> Unit
+}
+
+data class AnswerItem(val titleResourceId: Int, override val action: () -> Unit) : IAnswerItem {
+    override val title: Text get() = ResText(titleResourceId)
+}
+
+data class AnswerItem2(val titleString: String, override val action: () -> Unit) : IAnswerItem {
+    override val title: Text get() = CharSequenceText(titleString)
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt
index 6119120b82b..19cb5bd7c65 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.overlays
 
 import de.westnordost.streetcomplete.data.overlays.OverlayRegistry
 import de.westnordost.streetcomplete.overlays.address.AddressOverlay
+import de.westnordost.streetcomplete.overlays.cycleway.CyclewayOverlay
 import de.westnordost.streetcomplete.overlays.shops.ShopsOverlay
 import de.westnordost.streetcomplete.overlays.sidewalk.SidewalkOverlay
 import de.westnordost.streetcomplete.overlays.street_parking.StreetParkingOverlay
@@ -18,5 +19,6 @@ val overlaysModule = module {
         2 to StreetParkingOverlay(),
         3 to AddressOverlay(),
         4 to ShopsOverlay(get(named("FeatureDictionaryFuture"))),
+        5 to CyclewayOverlay(get(), get(named("CountryBoundariesFuture"))),
     )) }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/address/AddressOverlayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/address/AddressOverlayForm.kt
index 9f034a04fa1..88a7f86a625 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/overlays/address/AddressOverlayForm.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/address/AddressOverlayForm.kt
@@ -28,7 +28,7 @@ import de.westnordost.streetcomplete.osm.address.applyTo
 import de.westnordost.streetcomplete.osm.address.createAddressNumber
 import de.westnordost.streetcomplete.osm.address.streetHouseNumber
 import de.westnordost.streetcomplete.overlays.AbstractOverlayForm
-import de.westnordost.streetcomplete.quests.AnswerItem
+import de.westnordost.streetcomplete.overlays.AnswerItem
 import de.westnordost.streetcomplete.quests.road_name.RoadNameSuggestionsSource
 import de.westnordost.streetcomplete.util.getNameAndLocationLabel
 import de.westnordost.streetcomplete.util.ktx.isArea
diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/cycleway/CyclewayOverlay.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/cycleway/CyclewayOverlay.kt
new file mode 100644
index 00000000000..776824e73c4
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/cycleway/CyclewayOverlay.kt
@@ -0,0 +1,156 @@
+package de.westnordost.streetcomplete.overlays.cycleway
+
+import de.westnordost.countryboundaries.CountryBoundaries
+import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.meta.CountryInfo
+import de.westnordost.streetcomplete.data.meta.CountryInfos
+import de.westnordost.streetcomplete.data.meta.getByLocation
+import de.westnordost.streetcomplete.data.osm.mapdata.Element
+import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
+import de.westnordost.streetcomplete.data.osm.mapdata.filter
+import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement
+import de.westnordost.streetcomplete.osm.ALL_ROADS
+import de.westnordost.streetcomplete.osm.ANYTHING_UNPAVED
+import de.westnordost.streetcomplete.osm.MAXSPEED_TYPE_KEYS
+import de.westnordost.streetcomplete.osm.bicycle_boulevard.BicycleBoulevard
+import de.westnordost.streetcomplete.osm.bicycle_boulevard.createBicycleBoulevard
+import de.westnordost.streetcomplete.osm.cycleway.Cycleway
+import de.westnordost.streetcomplete.osm.cycleway.Cycleway.*
+import de.westnordost.streetcomplete.osm.cycleway.createCyclewaySides
+import de.westnordost.streetcomplete.osm.cycleway.isAmbiguous
+import de.westnordost.streetcomplete.osm.cycleway_separate.SeparateCycleway
+import de.westnordost.streetcomplete.osm.cycleway_separate.createSeparateCycleway
+import de.westnordost.streetcomplete.osm.isPrivateOnFoot
+import de.westnordost.streetcomplete.overlays.Color
+import de.westnordost.streetcomplete.overlays.Overlay
+import de.westnordost.streetcomplete.overlays.PolylineStyle
+import de.westnordost.streetcomplete.overlays.StrokeStyle
+import de.westnordost.streetcomplete.quests.cycleway.AddCycleway
+import java.util.concurrent.FutureTask
+
+class CyclewayOverlay(
+    private val countryInfos: CountryInfos,
+    private val countryBoundaries: FutureTask<CountryBoundaries>
+) : Overlay {
+
+    override val title = R.string.overlay_cycleway
+    override val icon = R.drawable.ic_quest_bicycleway
+    override val changesetComment = "Specify whether there are cycleways"
+    override val wikiLink: String = "Key:cycleway"
+    override val achievements = listOf(EditTypeAchievement.BICYCLIST)
+    override val hidesQuestTypes = setOf(AddCycleway::class.simpleName!!)
+
+    override fun getStyledElements(mapData: MapDataWithGeometry) =
+        // roads
+        mapData.filter("""
+            ways with
+              highway ~ ${ALL_ROADS.joinToString("|")}
+              and area != yes
+        """).mapNotNull {
+            val pos = mapData.getWayGeometry(it.id)?.center ?: return@mapNotNull null
+            val countryInfo = countryInfos.getByLocation(
+                countryBoundaries.get(),
+                pos.longitude,
+                pos.latitude
+            )
+            it to getStreetCyclewayStyle(it, countryInfo)
+        } +
+        // separately mapped ways
+        mapData.filter("""
+            ways with
+              highway ~ cycleway|path|footway
+              and area != yes
+        """).map { it to getSeparateCyclewayStyle(it) }
+
+    override fun createForm(element: Element?) =
+        if (element == null) null
+        else if (element.tags["highway"] in ALL_ROADS) StreetCyclewayOverlayForm()
+        else SeparateCyclewayForm()
+}
+
+private fun getSeparateCyclewayStyle(element: Element) =
+    PolylineStyle(StrokeStyle(createSeparateCycleway(element.tags).getColor()))
+
+private fun SeparateCycleway?.getColor() = when (this) {
+    SeparateCycleway.NONE,
+    SeparateCycleway.ALLOWED,
+    SeparateCycleway.NON_DESIGNATED ->
+        Color.BLACK
+
+    SeparateCycleway.NON_SEGREGATED ->
+        Color.CYAN
+
+    SeparateCycleway.SEGREGATED,
+    SeparateCycleway.EXCLUSIVE,
+    SeparateCycleway.EXCLUSIVE_WITH_SIDEWALK ->
+        Color.BLUE
+
+    null ->
+        Color.INVISIBLE
+}
+
+private fun getStreetCyclewayStyle(element: Element, countryInfo: CountryInfo): PolylineStyle {
+    val cycleways = createCyclewaySides(element.tags, countryInfo.isLeftHandTraffic)
+    val isBicycleBoulevard = createBicycleBoulevard(element.tags) == BicycleBoulevard.YES
+
+    // not set but on road that usually has no cycleway or it is private -> do not highlight as missing
+    val isNoCyclewayExpected =
+        cycleways == null && (cyclewayTaggingNotExpected(element) || isPrivateOnFoot(element))
+
+    return PolylineStyle(
+        stroke = when {
+            isBicycleBoulevard ->   StrokeStyle(Color.GOLD, dashed = true)
+            isNoCyclewayExpected -> StrokeStyle(Color.INVISIBLE)
+            else ->                 null
+        },
+        strokeLeft = if (isNoCyclewayExpected) null else cycleways?.left?.cycleway.getStyle(countryInfo),
+        strokeRight = if (isNoCyclewayExpected) null else cycleways?.right?.cycleway.getStyle(countryInfo)
+    )
+}
+
+private val cyclewayTaggingNotExpectedFilter by lazy { """
+    ways with
+      highway ~ track|living_street|pedestrian|service|motorway_link|motorway
+      or motorroad = yes
+      or expressway = yes
+      or maxspeed <= 20
+      or cyclestreet = yes
+      or bicycle_road = yes
+      or surface ~ ${ANYTHING_UNPAVED.joinToString("|")}
+      or ~${(MAXSPEED_TYPE_KEYS + "maxspeed").joinToString("|")} ~ ".*zone:?([1-9]|[1-2][0-9]|30)"
+""".toElementFilterExpression() }
+
+private fun cyclewayTaggingNotExpected(element: Element) =
+    cyclewayTaggingNotExpectedFilter.matches(element)
+
+private fun Cycleway?.getStyle(countryInfo: CountryInfo) = when (this) {
+    TRACK ->
+        StrokeStyle(Color.BLUE)
+
+    EXCLUSIVE_LANE, UNSPECIFIED_LANE ->
+        if (isAmbiguous(countryInfo)) StrokeStyle(Color.DATA_REQUESTED)
+        else                          StrokeStyle(Color.GOLD)
+
+    ADVISORY_LANE, SUGGESTION_LANE, UNSPECIFIED_SHARED_LANE ->
+        if (isAmbiguous(countryInfo)) StrokeStyle(Color.DATA_REQUESTED)
+        else                          StrokeStyle(Color.ORANGE)
+
+    PICTOGRAMS ->
+        StrokeStyle(Color.ORANGE, dashed = true)
+
+    UNKNOWN, INVALID, null, UNKNOWN_LANE, UNKNOWN_SHARED_LANE ->
+        StrokeStyle(Color.DATA_REQUESTED)
+
+    BUSWAY ->
+        StrokeStyle(Color.LIME, dashed = true)
+
+    SIDEWALK_EXPLICIT ->
+        StrokeStyle(Color.CYAN, dashed = true)
+
+    NONE, NONE_NO_ONEWAY, SHOULDER ->
+        StrokeStyle(Color.BLACK)
+
+    SEPARATE ->
+        StrokeStyle(Color.INVISIBLE)
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/cycleway/SeparateCyclewayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/cycleway/SeparateCyclewayForm.kt
new file mode 100644
index 00000000000..4e16cfbb514
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/cycleway/SeparateCyclewayForm.kt
@@ -0,0 +1,68 @@
+package de.westnordost.streetcomplete.overlays.cycleway
+
+import android.os.Bundle
+import android.view.View
+import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.UpdateElementTagsAction
+import de.westnordost.streetcomplete.osm.cycleway_separate.SeparateCycleway
+import de.westnordost.streetcomplete.osm.cycleway_separate.SeparateCycleway.*
+import de.westnordost.streetcomplete.osm.cycleway_separate.applyTo
+import de.westnordost.streetcomplete.osm.cycleway_separate.asItem
+import de.westnordost.streetcomplete.osm.cycleway_separate.createSeparateCycleway
+import de.westnordost.streetcomplete.overlays.AImageSelectOverlayForm
+import de.westnordost.streetcomplete.view.image_select.DisplayItem
+
+class SeparateCyclewayForm : AImageSelectOverlayForm<SeparateCycleway>() {
+
+    override val items: List<DisplayItem<SeparateCycleway>> get() =
+        listOf(NON_DESIGNATED, NON_SEGREGATED, SEGREGATED, EXCLUSIVE_WITH_SIDEWALK, EXCLUSIVE).map {
+            it.asItem(countryInfo.isLeftHandTraffic)
+        }
+
+    override val itemsPerRow = 1
+    override val cellLayoutId = R.layout.cell_labeled_icon_select_right
+
+    private var currentCycleway: SeparateCycleway? = null
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        val cycleway = createSeparateCycleway(element!!.tags)
+
+        /* Not displaying bicycle=yes and bicycle=no on footways and treating it the same because
+           whether riding a bike on a footway is allowed by default (without requiring signs) or
+           only under certain conditions (e.g. certain minimum width of sidewalk) is very much
+           dependent on the country or state one is in.
+
+           Hence, it is not verifiable well for the on-site surveyor: If there is no sign that
+           specifically allows or forbids cycling on a footway, the user is left with his loose
+           (mis)understanding of the local legislation to decide. After all, bicycle=yes/no
+           is (usually) nothing physical, but merely describes what is legal. It is in that sense
+           then not information surveyable on-the-ground, unless specifically signed.
+           bicycle=yes/no does however not make a statement about from where this info is derived.
+
+           So, from an on-site surveyor point of view, it is always better to record what is signed,
+           instead of what follows from that signage.
+
+           Signage, however, is out of scope of this overlay because while the physical presence of
+           a cycleway can be validated at a glance, the presence of a sign requires to walk a bit up
+           or down the street in order to find (or not find) a sign.
+           More importantly, at the time of writing, there is no way to tag the information that a
+           bicycle=* access restriction is derived from the presence of a sign. This however is a
+           prerequisite for it being  displayed as a selectable option due to the reasons stated
+           above.
+        */
+        currentCycleway = if (cycleway == NONE || cycleway == ALLOWED) NON_DESIGNATED else cycleway
+        selectedItem = currentCycleway?.asItem(countryInfo.isLeftHandTraffic)
+    }
+
+    override fun hasChanges(): Boolean =
+        selectedItem?.value != currentCycleway
+
+    override fun onClickOk() {
+        val tagChanges = StringMapChangesBuilder(element!!.tags)
+        selectedItem!!.value!!.applyTo(tagChanges)
+        applyEdit(UpdateElementTagsAction(tagChanges.create()))
+    }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/cycleway/StreetCyclewayOverlayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/cycleway/StreetCyclewayOverlayForm.kt
new file mode 100644
index 00000000000..bb3ee5fff88
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/cycleway/StreetCyclewayOverlayForm.kt
@@ -0,0 +1,241 @@
+package de.westnordost.streetcomplete.overlays.cycleway
+
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.UpdateElementTagsAction
+import de.westnordost.streetcomplete.osm.bicycle_boulevard.BicycleBoulevard
+import de.westnordost.streetcomplete.osm.bicycle_boulevard.applyTo
+import de.westnordost.streetcomplete.osm.bicycle_boulevard.createBicycleBoulevard
+import de.westnordost.streetcomplete.osm.cycleway.Cycleway
+import de.westnordost.streetcomplete.osm.cycleway.CyclewayAndDirection
+import de.westnordost.streetcomplete.osm.cycleway.LeftAndRightCycleway
+import de.westnordost.streetcomplete.osm.cycleway.applyTo
+import de.westnordost.streetcomplete.osm.cycleway.asDialogItem
+import de.westnordost.streetcomplete.osm.cycleway.asStreetSideItem
+import de.westnordost.streetcomplete.osm.cycleway.createCyclewaySides
+import de.westnordost.streetcomplete.osm.cycleway.getSelectableCycleways
+import de.westnordost.streetcomplete.osm.cycleway.selectableOrNullValues
+import de.westnordost.streetcomplete.osm.cycleway.wasNoOnewayForCyclistsButNowItIs
+import de.westnordost.streetcomplete.osm.isOneway
+import de.westnordost.streetcomplete.osm.isReversedOneway
+import de.westnordost.streetcomplete.overlays.AStreetSideSelectOverlayForm
+import de.westnordost.streetcomplete.overlays.AnswerItem
+import de.westnordost.streetcomplete.overlays.AnswerItem2
+import de.westnordost.streetcomplete.overlays.IAnswerItem
+import de.westnordost.streetcomplete.util.ktx.toast
+import de.westnordost.streetcomplete.view.controller.StreetSideDisplayItem
+import de.westnordost.streetcomplete.view.image_select.ImageListPickerDialog
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+
+class StreetCyclewayOverlayForm : AStreetSideSelectOverlayForm<CyclewayAndDirection>() {
+
+    override val otherAnswers: List<IAnswerItem> get() =
+        listOfNotNull(
+            createSwitchBicycleBoulevardAnswer(),
+            createReverseCyclewayDirectionAnswer()
+        )
+
+    private var originalCycleway: LeftAndRightCycleway? = null
+    private var originalBicycleBoulevard: BicycleBoulevard = BicycleBoulevard.NO
+    private var bicycleBoulevard: BicycleBoulevard = BicycleBoulevard.NO
+    private var reverseDirection: Boolean = false
+
+    /** returns whether the side that goes into the opposite direction of the driving direction of a
+     * one-way is on the right side of the way */
+    private val isReverseSideRight get() = isReversedOneway xor isLeftHandTraffic
+
+    private val isOneway get() = isOneway(element!!.tags)
+    private val isReversedOneway get() = isReversedOneway(element!!.tags)
+
+    // just a shortcut
+    private val isLeftHandTraffic get() = countryInfo.isLeftHandTraffic
+
+    private fun isContraflowInOneway(isRight: Boolean): Boolean =
+        isOneway && (isReverseSideRight == isRight)
+
+    /* ---------------------------------------- lifecycle --------------------------------------- */
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        originalCycleway = createCyclewaySides(element!!.tags, isLeftHandTraffic)?.selectableOrNullValues(countryInfo)
+        originalBicycleBoulevard = createBicycleBoulevard(element!!.tags)
+
+        if (savedInstanceState == null) {
+            initStateFromTags()
+        } else {
+            savedInstanceState.getString(BICYCLE_BOULEVARD)?.let {
+                bicycleBoulevard = BicycleBoulevard.valueOf(it)
+            }
+        }
+        updateBicycleBoulevard()
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        outState.putString(BICYCLE_BOULEVARD, bicycleBoulevard.name)
+    }
+
+    private fun initStateFromTags() {
+        bicycleBoulevard = originalBicycleBoulevard
+
+        val leftItem = originalCycleway?.left?.asStreetSideItem(false, isContraflowInOneway(false), countryInfo)
+        streetSideSelect.setPuzzleSide(leftItem, false)
+
+        val rightItem = originalCycleway?.right?.asStreetSideItem(true, isContraflowInOneway(true), countryInfo)
+        streetSideSelect.setPuzzleSide(rightItem, true)
+    }
+
+    /* ----------------------------------- bicycle boulevards ----------------------------------- */
+
+    private fun createSwitchBicycleBoulevardAnswer(): IAnswerItem? =
+        if (bicycleBoulevard == BicycleBoulevard.YES) {
+            AnswerItem2(
+                getString(R.string.bicycle_boulevard_is_not_a, getString(R.string.bicycle_boulevard)),
+                ::removeBicycleBoulevard
+            )
+        } else if (countryInfo.hasBicycleBoulevard) {
+            AnswerItem2(
+                getString(R.string.bicycle_boulevard_is_a, getString(R.string.bicycle_boulevard)),
+                ::addBicycleBoulevard
+            )
+        } else {
+            null
+        }
+
+    private fun removeBicycleBoulevard() {
+        bicycleBoulevard = BicycleBoulevard.NO
+        updateBicycleBoulevard()
+    }
+
+    private fun addBicycleBoulevard() {
+        bicycleBoulevard = BicycleBoulevard.YES
+        updateBicycleBoulevard()
+    }
+
+    private fun updateBicycleBoulevard() {
+        val bicycleBoulevardSignView = requireView().findViewById<View>(R.id.signBicycleBoulevard)
+        if (bicycleBoulevard == BicycleBoulevard.YES) {
+            if (bicycleBoulevardSignView == null) {
+                layoutInflater.inflate(
+                    R.layout.sign_bicycle_boulevard,
+                    requireView().findViewById(R.id.content), true
+                )
+            }
+        } else {
+            (bicycleBoulevardSignView?.parent as? ViewGroup)?.removeView(bicycleBoulevardSignView)
+        }
+    }
+
+    /* ------------------------------ reverse cycleway direction -------------------------------- */
+
+    private fun createReverseCyclewayDirectionAnswer(): IAnswerItem? =
+        if (bicycleBoulevard == BicycleBoulevard.YES) null
+        else AnswerItem(R.string.cycleway_reverse_direction, ::selectReverseCyclewayDirection)
+
+    private fun selectReverseCyclewayDirection() {
+        confirmSelectReverseCyclewayDirection {
+            reverseDirection = true
+            context?.toast(R.string.cycleway_reverse_direction_toast)
+        }
+    }
+
+    private fun confirmSelectReverseCyclewayDirection(callback: () -> Unit) {
+        AlertDialog.Builder(requireContext())
+            .setTitle(R.string.quest_generic_confirmation_title)
+            .setMessage(R.string.cycleway_reverse_direction_warning)
+            .setPositiveButton(R.string.quest_generic_confirmation_yes) { _, _ -> callback() }
+            .setNegativeButton(R.string.quest_generic_confirmation_no, null)
+            .show()
+    }
+
+    private fun reverseCyclewayDirection(isRight: Boolean) {
+        val isContraflowInOneway = isContraflowInOneway(isRight)
+        reverseDirection = false
+        val value = streetSideSelect.getPuzzleSide(isRight)?.value ?: return
+        val newValue = value.copy(direction = value.direction.reverse())
+        val newItem = newValue.asStreetSideItem(isRight, isContraflowInOneway, countryInfo)
+        streetSideSelect.replacePuzzleSide(newItem, isRight)
+    }
+
+    /* --------------------------------- select & apply answer ---------------------------------- */
+
+    override fun onClickSide(isRight: Boolean) {
+        if (reverseDirection) {
+            reverseCyclewayDirection(isRight)
+        } else {
+            selectCycleway(isRight)
+        }
+    }
+
+    private fun selectCycleway(isRight: Boolean) {
+        val isContraflowInOneway = isContraflowInOneway(isRight)
+        val dialogItems = getSelectableCycleways(countryInfo, element!!.tags, isRight)
+            .map { it.asDialogItem(isRight, isContraflowInOneway, requireContext(), countryInfo) }
+
+        ImageListPickerDialog(requireContext(), dialogItems, R.layout.labeled_icon_button_cell, 2) { item ->
+            val streetSideItem = item.value!!.asStreetSideItem(isRight, isContraflowInOneway, countryInfo)
+            streetSideSelect.replacePuzzleSide(streetSideItem, isRight)
+        }.show()
+    }
+
+    override fun onClickOk() {
+        if (bicycleBoulevard == BicycleBoulevard.YES) {
+            val tags = StringMapChangesBuilder(element!!.tags)
+            bicycleBoulevard.applyTo(tags, countryInfo.countryCode)
+            applyEdit(UpdateElementTagsAction(tags.create()))
+        } else {
+            // only tag the cycleway if that is what is currently displayed
+            val cycleways = LeftAndRightCycleway(streetSideSelect.left?.value, streetSideSelect.right?.value)
+            if (cycleways.wasNoOnewayForCyclistsButNowItIs(element!!.tags, isLeftHandTraffic)) {
+                confirmNotOnewayForCyclists { saveAndApplyCycleway(cycleways) }
+            } else {
+                saveAndApplyCycleway(cycleways)
+            }
+        }
+    }
+
+    private fun confirmNotOnewayForCyclists(callback: () -> Unit) {
+        AlertDialog.Builder(requireContext())
+            .setMessage(R.string.quest_cycleway_confirmation_oneway_for_cyclists_too)
+            .setPositiveButton(R.string.quest_generic_confirmation_yes) { _, _ -> callback() }
+            .setNegativeButton(R.string.quest_generic_confirmation_no, null)
+            .show()
+    }
+
+    private fun saveAndApplyCycleway(cycleways: LeftAndRightCycleway) {
+        streetSideSelect.saveLastSelection()
+        val tags = StringMapChangesBuilder(element!!.tags)
+        cycleways.applyTo(tags, countryInfo.isLeftHandTraffic)
+        bicycleBoulevard.applyTo(tags, countryInfo.countryCode)
+        applyEdit(UpdateElementTagsAction(tags.create()))
+    }
+
+    /* ----------------------------- AStreetSideSelectOverlayForm ------------------------------- */
+
+    override fun hasChanges(): Boolean =
+        streetSideSelect.left?.value != originalCycleway?.left  ||
+        streetSideSelect.right?.value != originalCycleway?.right ||
+        originalBicycleBoulevard != bicycleBoulevard
+
+    override fun serialize(item: CyclewayAndDirection) = Json.encodeToString(item)
+    override fun deserialize(str: String) = Json.decodeFromString<CyclewayAndDirection>(str)
+    override fun asStreetSideItem(item: CyclewayAndDirection, isRight: Boolean): StreetSideDisplayItem<CyclewayAndDirection> {
+        val isContraflowInOneway = isContraflowInOneway(isRight)
+        // NONE_NO_ONEWAY is displayed as simply NONE if not in contraflow because the former makes
+        // only really sense in contraflow. This can only happen when applying the side(s) via the
+        // last answer button
+        val item2 = if (item.cycleway == Cycleway.NONE_NO_ONEWAY && !isContraflowInOneway) item.copy(cycleway = Cycleway.NONE) else item
+        return item2.asStreetSideItem(isRight, isContraflowInOneway, countryInfo)
+    }
+
+    companion object {
+        private const val BICYCLE_BOULEVARD = "bicycle_boulevard"
+    }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlayForm.kt
index a9d93c3c9d8..cadd68b2a6d 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlayForm.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlayForm.kt
@@ -26,7 +26,7 @@ import de.westnordost.streetcomplete.osm.applyTo
 import de.westnordost.streetcomplete.osm.createLocalizedNames
 import de.westnordost.streetcomplete.osm.replaceShop
 import de.westnordost.streetcomplete.overlays.AbstractOverlayForm
-import de.westnordost.streetcomplete.quests.AnswerItem
+import de.westnordost.streetcomplete.overlays.AnswerItem
 import de.westnordost.streetcomplete.quests.LocalizedNameAdapter
 import de.westnordost.streetcomplete.util.getLocalesForFeatureDictionary
 import de.westnordost.streetcomplete.util.getLocationLabel
diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/sidewalk/SidewalkOverlay.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/sidewalk/SidewalkOverlay.kt
index 00224fe8a5d..09d1090ddfe 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/overlays/sidewalk/SidewalkOverlay.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/sidewalk/SidewalkOverlay.kt
@@ -1,14 +1,20 @@
 package de.westnordost.streetcomplete.overlays.sidewalk
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.osm.ALL_ROADS
+import de.westnordost.streetcomplete.osm.ANYTHING_UNPAVED
+import de.westnordost.streetcomplete.osm.cycleway_separate.SeparateCycleway
+import de.westnordost.streetcomplete.osm.cycleway_separate.createSeparateCycleway
 import de.westnordost.streetcomplete.osm.isPrivateOnFoot
 import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk
+import de.westnordost.streetcomplete.osm.sidewalk.any
 import de.westnordost.streetcomplete.osm.sidewalk.createSidewalkSides
+import de.westnordost.streetcomplete.overlays.AbstractOverlayForm
 import de.westnordost.streetcomplete.overlays.Color
 import de.westnordost.streetcomplete.overlays.Overlay
 import de.westnordost.streetcomplete.overlays.PolylineStyle
@@ -31,24 +37,47 @@ class SidewalkOverlay : Overlay {
               highway ~ motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified|residential|living_street|pedestrian|service
               and area != yes
         """).map { it to getSidewalkStyle(it) } +
-        // footways etc, just to highlight e.g. separately mapped sidewalks
+        // footways etc, just to highlight e.g. separately mapped sidewalks. However, it is also
+        // possible to add sidewalks to them. At least in NL, cycleways with sidewalks actually exist
         mapData.filter("""
             ways with (
-              highway ~ footway|steps
-              or highway ~ path|bridleway|cycleway and foot ~ yes|designated
+              highway ~ footway|steps|path|bridleway|cycleway
             ) and area != yes
-        """).map { it to PolylineStyle(StrokeStyle(Color.SKY)) }
+        """).map { it to getFootwayStyle(it) }
 
-    override fun createForm(element: Element?) =
-        if (element != null && element.tags["highway"] in ALL_ROADS) SidewalkOverlayForm()
-        else null
+    override fun createForm(element: Element?): AbstractOverlayForm? {
+        if (element == null) return null
+
+        // allow editing of all roads and all exclusive cycleways
+        return if (
+            element.tags["highway"] in ALL_ROADS ||
+            createSeparateCycleway(element.tags) in listOf(SeparateCycleway.EXCLUSIVE, SeparateCycleway.EXCLUSIVE_WITH_SIDEWALK)
+        ) SidewalkOverlayForm() else null
+    }
+}
+
+private fun getFootwayStyle(element: Element): PolylineStyle {
+    val foot = element.tags["foot"] ?: when (element.tags["highway"]) {
+        "footway" -> "designated"
+        "path" -> "yes"
+        else -> null
+    }
+
+    return when {
+        createSidewalkSides(element.tags)?.any { it == Sidewalk.YES } == true ->
+            getSidewalkStyle(element)
+        foot in listOf("yes", "designated") ->
+            PolylineStyle(StrokeStyle(Color.SKY))
+        else ->
+            PolylineStyle(StrokeStyle(Color.INVISIBLE))
+    }
 }
 
 private fun getSidewalkStyle(element: Element): PolylineStyle {
     val sidewalkSides = createSidewalkSides(element.tags)
     // not set but on road that usually has no sidewalk or it is private -> do not highlight as missing
     if (sidewalkSides == null) {
-        if (sidewalkTaggingNotExpected(element.tags) || isPrivateOnFoot(element)) {
+        if (sidewalkTaggingNotExpected(element) || isPrivateOnFoot(element)) {
             return PolylineStyle(StrokeStyle(Color.INVISIBLE))
         }
     }
@@ -60,13 +89,17 @@ private fun getSidewalkStyle(element: Element): PolylineStyle {
     )
 }
 
-private val highwayValuesWhereSidewalkTaggingIsNotExpected = setOf(
-    "living_street", "pedestrian", "service", "motorway_link"
-)
+private val sidewalkTaggingNotExpectedFilter by lazy { """
+    ways with
+      highway ~ living_street|pedestrian|service|motorway_link
+      or motorroad = yes
+      or expressway = yes
+      or maxspeed <= 10
+      or surface ~ ${ANYTHING_UNPAVED.joinToString("|")}
+""".toElementFilterExpression() }
 
-private fun sidewalkTaggingNotExpected(tags: Map<String, String>): Boolean =
-    tags["highway"] in highwayValuesWhereSidewalkTaggingIsNotExpected
-        || tags["motorroad"] == "yes" || tags["expressway"] == "yes"
+private fun sidewalkTaggingNotExpected(element: Element) =
+    sidewalkTaggingNotExpectedFilter.matches(element)
 
 private val Sidewalk?.style get() = StrokeStyle(when (this) {
     Sidewalk.YES           -> Color.SKY
diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/sidewalk/SidewalkOverlayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/sidewalk/SidewalkOverlayForm.kt
index 6cbca6a40ba..afcfbc7854c 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/overlays/sidewalk/SidewalkOverlayForm.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/sidewalk/SidewalkOverlayForm.kt
@@ -24,13 +24,13 @@ class SidewalkOverlayForm : AStreetSideSelectOverlayForm<Sidewalk>() {
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
+        originalSidewalk = createSidewalkSides(element!!.tags)?.validOrNullValues()
         if (savedInstanceState == null) {
             initStateFromTags()
         }
     }
 
     private fun initStateFromTags() {
-        originalSidewalk = createSidewalkSides(element!!.tags)?.validOrNullValues()
         streetSideSelect.setPuzzleSide(originalSidewalk?.left?.asStreetSideItem(), false)
         streetSideSelect.setPuzzleSide(originalSidewalk?.right?.asStreetSideItem(), true)
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/street_parking/StreetParkingOverlayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/street_parking/StreetParkingOverlayForm.kt
index 8feaf54b838..6e7a1c55fe9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/overlays/street_parking/StreetParkingOverlayForm.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/street_parking/StreetParkingOverlayForm.kt
@@ -66,13 +66,13 @@ class StreetParkingOverlayForm : AStreetSideSelectOverlayForm<StreetParking>() {
             getString(R.string.street_parking_street_width, widthFormatted)
         } else null
 
+        originalParking = createStreetParkingSides(element!!.tags)?.validOrNullValues()
         if (savedInstanceState == null) {
             initStateFromTags()
         }
     }
 
     private fun initStateFromTags() {
-        originalParking = createStreetParkingSides(element!!.tags)?.validOrNullValues()
         streetSideSelect.setPuzzleSide(originalParking?.left?.asStreetSideItem(requireContext(), isUpsideDown(false)), false)
         streetSideSelect.setPuzzleSide(originalParking?.right?.asStreetSideItem(requireContext(), isUpsideDown(true)), true)
     }
@@ -129,7 +129,7 @@ class StreetParkingOverlayForm : AStreetSideSelectOverlayForm<StreetParking>() {
     /* --------------------------------------- apply answer ------------------------------------- */
 
     override fun onClickOk() {
-        if (streetSideSelect.isComplete) streetSideSelect.saveLastSelection()
+        streetSideSelect.saveLastSelection()
         val parking = LeftAndRightStreetParking(streetSideSelect.left?.value, streetSideSelect.right?.value)
         val tagChanges = StringMapChangesBuilder(element!!.tags)
         parking.applyTo(tagChanges)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/AbstractOsmQuestForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/AbstractOsmQuestForm.kt
index 5777b38b0b9..b0b42fff090 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/AbstractOsmQuestForm.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/AbstractOsmQuestForm.kt
@@ -42,6 +42,7 @@ import de.westnordost.streetcomplete.util.getNameAndLocationLabel
 import de.westnordost.streetcomplete.util.ktx.geometryType
 import de.westnordost.streetcomplete.util.ktx.isSplittable
 import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope
+import de.westnordost.streetcomplete.view.add
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -82,8 +83,8 @@ abstract class AbstractOsmQuestForm<T> : AbstractQuestForm(), IsShowingQuestDeta
         }
 
     // overridable by child classes
-    open val otherAnswers = listOf<AnswerItem>()
-    open val buttonPanelAnswers = listOf<AnswerItem>()
+    open val otherAnswers = listOf<IAnswerItem>()
+    open val buttonPanelAnswers = listOf<IAnswerItem>()
 
     interface Listener {
         /** The GPS position at which the user is displayed at */
@@ -135,8 +136,8 @@ abstract class AbstractOsmQuestForm<T> : AbstractQuestForm(), IsShowingQuestDeta
         setButtonPanelAnswers(listOf(otherAnswersItem) + buttonPanelAnswers)
     }
 
-    private fun assembleOtherAnswers(): List<AnswerItem> {
-        val answers = mutableListOf<AnswerItem>()
+    private fun assembleOtherAnswers(): List<IAnswerItem> {
+        val answers = mutableListOf<IAnswerItem>()
 
         answers.add(AnswerItem(R.string.quest_generic_answer_notApplicable) { onClickCantSay() })
 
@@ -176,7 +177,7 @@ abstract class AbstractOsmQuestForm<T> : AbstractQuestForm(), IsShowingQuestDeta
         for (i in answers.indices) {
             val otherAnswer = answers[i]
             val order = answers.size - i
-            popup.menu.add(Menu.NONE, i, order, otherAnswer.titleResourceId)
+            popup.menu.add(Menu.NONE, i, order, otherAnswer.title)
         }
         popup.show()
 
@@ -226,7 +227,7 @@ abstract class AbstractOsmQuestForm<T> : AbstractQuestForm(), IsShowingQuestDeta
 
     private fun createQuestChanges(answer: T): StringMapChanges {
         val changesBuilder = StringMapChangesBuilder(element.tags)
-        osmElementQuestType.applyAnswerTo(answer, changesBuilder, element.timestampEdited)
+        osmElementQuestType.applyAnswerTo(answer, changesBuilder, geometry, element.timestampEdited)
         val changes = changesBuilder.create()
         require(!changes.isEmpty()) {
             "${osmElementQuestType.name} was answered by the user but there are no changes!"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/AbstractQuestForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/AbstractQuestForm.kt
index cc8f6948a65..3169ba1424d 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/AbstractQuestForm.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/AbstractQuestForm.kt
@@ -27,6 +27,10 @@ import de.westnordost.streetcomplete.util.ktx.popIn
 import de.westnordost.streetcomplete.util.ktx.popOut
 import de.westnordost.streetcomplete.util.ktx.toast
 import de.westnordost.streetcomplete.util.ktx.updateConfiguration
+import de.westnordost.streetcomplete.view.CharSequenceText
+import de.westnordost.streetcomplete.view.ResText
+import de.westnordost.streetcomplete.view.Text
+import de.westnordost.streetcomplete.view.setText
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
@@ -189,11 +193,11 @@ abstract class AbstractQuestForm :
         }
     }
 
-    protected fun setButtonPanelAnswers(buttonPanelAnswers: List<AnswerItem>) {
+    protected fun setButtonPanelAnswers(buttonPanelAnswers: List<IAnswerItem>) {
         binding.buttonPanel.removeAllViews()
         for (buttonPanelAnswer in buttonPanelAnswers) {
             val button = ButtonPanelButtonBinding.inflate(layoutInflater, binding.buttonPanel, true).root
-            button.setText(buttonPanelAnswer.titleResourceId)
+            button.setText(buttonPanelAnswer.title)
             button.setOnClickListener { buttonPanelAnswer.action() }
         }
     }
@@ -237,4 +241,15 @@ abstract class AbstractQuestForm :
     }
 }
 
-data class AnswerItem(val titleResourceId: Int, val action: () -> Unit)
+interface IAnswerItem {
+    val title: Text
+    val action: () -> Unit
+}
+
+data class AnswerItem(val titleResourceId: Int, override val action: () -> Unit) : IAnswerItem {
+    override val title: Text get() = ResText(titleResourceId)
+}
+
+data class AnswerItem2(val titleString: String, override val action: () -> Unit) : IAnswerItem {
+    override val title: Text get() = CharSequenceText(titleString)
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/accepts_cards/AddAcceptsCards.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/accepts_cards/AddAcceptsCards.kt
index f0b4b4aad7e..b3a7d1897d5 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/accepts_cards/AddAcceptsCards.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/accepts_cards/AddAcceptsCards.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.accepts_cards
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -37,7 +38,7 @@ class AddAcceptsCards : OsmFilterQuestType<CardAcceptance>() {
 
     override fun createForm() = AddAcceptsCardsForm()
 
-    override fun applyAnswerTo(answer: CardAcceptance, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: CardAcceptance, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["payment:debit_cards"] = answer.debit.toYesNo()
         tags["payment:credit_cards"] = answer.credit.toYesNo()
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/accepts_cash/AddAcceptsCash.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/accepts_cash/AddAcceptsCash.kt
index b5078c0b9d9..27955458125 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/accepts_cash/AddAcceptsCash.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/accepts_cash/AddAcceptsCash.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.accepts_cash
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -68,7 +69,7 @@ class AddAcceptsCash : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["payment:cash"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/access_point_ref/AddAccessPointRef.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/access_point_ref/AddAccessPointRef.kt
index 828a4f944fe..8e96252ce2e 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/access_point_ref/AddAccessPointRef.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/access_point_ref/AddAccessPointRef.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.access_point_ref
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement
 import de.westnordost.streetcomplete.osm.Tags
@@ -25,7 +26,7 @@ class AddAccessPointRef : OsmFilterQuestType<AccessPointRefAnswer>() {
 
     override fun createForm() = AddAccessPointRefForm()
 
-    override fun applyAnswerTo(answer: AccessPointRefAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: AccessPointRefAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is NoVisibleAccessPointRef -> tags["ref:signed"] = "no"
             is AccessPointRef ->          tags["ref"] = answer.ref
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddAddressStreet.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddAddressStreet.kt
index d9af46ab418..76ee3567386 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddAddressStreet.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddAddressStreet.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.address
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.ElementType
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
@@ -62,7 +63,7 @@ class AddAddressStreet : OsmElementQuestType<StreetOrPlaceName> {
 
     override fun createForm() = AddAddressStreetForm()
 
-    override fun applyAnswerTo(answer: StreetOrPlaceName, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: StreetOrPlaceName, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         answer.applyTo(tags)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddHousenumber.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddHousenumber.kt
index ae5343a0339..8ebc82d4e69 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddHousenumber.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddHousenumber.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.address
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.geometry.ElementPolygonsGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.ElementType
@@ -141,7 +142,7 @@ class AddHousenumber : OsmElementQuestType<HouseNumberAnswer> {
 
     override fun createForm() = AddHousenumberForm()
 
-    override fun applyAnswerTo(answer: HouseNumberAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: HouseNumberAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is AddressNumberOrName -> {
                 if (answer.number == null && answer.name == null) {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/air_conditioning/AddAirConditioning.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/air_conditioning/AddAirConditioning.kt
index d538105af79..989e06f9692 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/air_conditioning/AddAirConditioning.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/air_conditioning/AddAirConditioning.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.air_conditioning
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -36,7 +37,7 @@ class AddAirConditioning : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["air_conditioning"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/air_pump/AddAirCompressor.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/air_pump/AddAirCompressor.kt
index 1b27a7bec28..98ccd2c841b 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/air_pump/AddAirCompressor.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/air_pump/AddAirCompressor.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.air_pump
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -40,7 +41,7 @@ class AddAirCompressor : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("compressed_air", answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/air_pump/AddBicyclePump.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/air_pump/AddBicyclePump.kt
index 967840abff9..92aa4c73dd8 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/air_pump/AddBicyclePump.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/air_pump/AddBicyclePump.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.air_pump
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -48,7 +49,7 @@ class AddBicyclePump : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("service:bicycle:pump", answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/atm_cashin/AddAtmCashIn.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/atm_cashin/AddAtmCashIn.kt
index b9a273d700b..310b53232ba 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/atm_cashin/AddAtmCashIn.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/atm_cashin/AddAtmCashIn.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.atm_cashin
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -26,7 +27,7 @@ class AddAtmCashIn : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["cash_in"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/atm_operator/AddAtmOperator.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/atm_operator/AddAtmOperator.kt
index 0375caebfee..9b29d14e81c 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/atm_operator/AddAtmOperator.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/atm_operator/AddAtmOperator.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.atm_operator
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -24,7 +25,7 @@ class AddAtmOperator : OsmFilterQuestType<String>() {
 
     override fun createForm() = AddAtmOperatorForm()
 
-    override fun applyAnswerTo(answer: String, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: String, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["operator"] = answer
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/baby_changing_table/AddBabyChangingTable.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/baby_changing_table/AddBabyChangingTable.kt
index b8644a2dec7..2466ecd9ba1 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/baby_changing_table/AddBabyChangingTable.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/baby_changing_table/AddBabyChangingTable.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.baby_changing_table
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
 import de.westnordost.streetcomplete.osm.Tags
@@ -31,7 +32,7 @@ class AddBabyChangingTable : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["changing_table"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_bicycle_barrier_installation/AddBicycleBarrierInstallation.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_bicycle_barrier_installation/AddBicycleBarrierInstallation.kt
index 03a7f783a22..aba5448eafc 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_bicycle_barrier_installation/AddBicycleBarrierInstallation.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_bicycle_barrier_installation/AddBicycleBarrierInstallation.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.barrier_bicycle_barrier_installation
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -31,7 +32,7 @@ class AddBicycleBarrierInstallation : OsmFilterQuestType<BicycleBarrierInstallat
 
     override fun createForm() = AddBicycleBarrierInstallationForm()
 
-    override fun applyAnswerTo(answer: BicycleBarrierInstallationAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BicycleBarrierInstallationAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is BicycleBarrierInstallation -> tags["cycle_barrier:installation"] = answer.osmValue
             BarrierTypeIsNotBicycleBarrier -> tags["barrier"] = "yes"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_bicycle_barrier_type/AddBicycleBarrierType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_bicycle_barrier_type/AddBicycleBarrierType.kt
index 961188bf45e..c4732884210 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_bicycle_barrier_type/AddBicycleBarrierType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_bicycle_barrier_type/AddBicycleBarrierType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.barrier_bicycle_barrier_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BLIND
@@ -21,7 +22,7 @@ class AddBicycleBarrierType : OsmFilterQuestType<BicycleBarrierTypeAnswer>() {
 
     override fun createForm() = AddBicycleBarrierTypeForm()
 
-    override fun applyAnswerTo(answer: BicycleBarrierTypeAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BicycleBarrierTypeAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is BicycleBarrierType -> tags["cycle_barrier"] = answer.osmValue
             BarrierTypeIsNotBicycleBarrier -> tags["barrier"] = "yes"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierOnPath.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierOnPath.kt
index ed664cb215f..67d56a72fac 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierOnPath.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierOnPath.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.barrier_type
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Node
@@ -41,6 +42,6 @@ class AddBarrierOnPath : OsmElementQuestType<BarrierType> {
 
     override fun createForm() = AddBarrierTypeForm()
 
-    override fun applyAnswerTo(answer: BarrierType, tags: Tags, timestampEdited: Long) =
+    override fun applyAnswerTo(answer: BarrierType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) =
         answer.applyTo(tags)
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierOnRoad.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierOnRoad.kt
index 60cebee750b..08fd1dabc2f 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierOnRoad.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierOnRoad.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.barrier_type
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Node
@@ -40,6 +41,6 @@ class AddBarrierOnRoad : OsmElementQuestType<BarrierType> {
 
     override fun createForm() = AddBarrierTypeForm()
 
-    override fun applyAnswerTo(answer: BarrierType, tags: Tags, timestampEdited: Long) =
+    override fun applyAnswerTo(answer: BarrierType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) =
         answer.applyTo(tags)
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierType.kt
index 2d3cca7c702..4a3fd40c502 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddBarrierType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.barrier_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BLIND
@@ -35,6 +36,6 @@ class AddBarrierType : OsmFilterQuestType<BarrierType>() {
 
     override fun createForm() = AddBarrierTypeForm()
 
-    override fun applyAnswerTo(answer: BarrierType, tags: Tags, timestampEdited: Long) =
+    override fun applyAnswerTo(answer: BarrierType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) =
         answer.applyTo(tags)
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddStileType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddStileType.kt
index 2bf7a34ea7d..cc31e291622 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddStileType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/barrier_type/AddStileType.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.barrier_type
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -47,7 +48,7 @@ class AddStileType : OsmElementQuestType<StileTypeAnswer> {
 
     override fun createForm() = AddStileTypeForm()
 
-    override fun applyAnswerTo(answer: StileTypeAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: StileTypeAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is StileType -> {
                 val newType = answer.osmValue
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bench_backrest/AddBenchBackrest.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bench_backrest/AddBenchBackrest.kt
index 6a5713a887f..c5e0c34fcfb 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bench_backrest/AddBenchBackrest.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bench_backrest/AddBenchBackrest.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bench_backrest
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -35,7 +36,7 @@ class AddBenchBackrest : OsmFilterQuestType<BenchBackrestAnswer>() {
 
     override fun createForm() = AddBenchBackrestForm()
 
-    override fun applyAnswerTo(answer: BenchBackrestAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BenchBackrestAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             PICNIC_TABLE -> {
                 tags["leisure"] = "picnic_table"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_capacity/AddBikeParkingCapacity.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_capacity/AddBikeParkingCapacity.kt
index 067a3c491a7..13fdaf30b6d 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_capacity/AddBikeParkingCapacity.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_capacity/AddBikeParkingCapacity.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bike_parking_capacity
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -38,7 +39,7 @@ class AddBikeParkingCapacity : OsmFilterQuestType<Int>() {
 
     override fun createForm() = AddBikeParkingCapacityForm.create(showClarificationText = true)
 
-    override fun applyAnswerTo(answer: Int, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Int, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("capacity", answer.toString())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_cover/AddBikeParkingCover.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_cover/AddBikeParkingCover.kt
index 039b31641ce..16e95945485 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_cover/AddBikeParkingCover.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_cover/AddBikeParkingCover.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bike_parking_cover
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -32,7 +33,7 @@ class AddBikeParkingCover : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["covered"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_type/AddBikeParkingType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_type/AddBikeParkingType.kt
index 1ff3c747c6f..03ddc252733 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_type/AddBikeParkingType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_parking_type/AddBikeParkingType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bike_parking_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -29,7 +30,7 @@ class AddBikeParkingType : OsmFilterQuestType<BikeParkingType>() {
 
     override fun createForm() = AddBikeParkingTypeForm()
 
-    override fun applyAnswerTo(answer: BikeParkingType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BikeParkingType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["bicycle_parking"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_rental_capacity/AddBikeRentalCapacity.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_rental_capacity/AddBikeRentalCapacity.kt
index a5aaea831f6..20eeb34f909 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_rental_capacity/AddBikeRentalCapacity.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_rental_capacity/AddBikeRentalCapacity.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bike_rental_capacity
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -33,7 +34,7 @@ class AddBikeRentalCapacity : OsmFilterQuestType<Int>() {
 
     override fun createForm() = AddBikeParkingCapacityForm.create(showClarificationText = false)
 
-    override fun applyAnswerTo(answer: Int, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Int, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("capacity", answer.toString())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_rental_type/AddBikeRentalType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_rental_type/AddBikeRentalType.kt
index 74caca618e1..fffc16d5e2a 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_rental_type/AddBikeRentalType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_rental_type/AddBikeRentalType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bike_rental_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -30,7 +31,7 @@ class AddBikeRentalType : OsmFilterQuestType<BikeRentalTypeAnswer>() {
 
     override fun createForm() = AddBikeRentalTypeForm()
 
-    override fun applyAnswerTo(answer: BikeRentalTypeAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BikeRentalTypeAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is BikeRentalType -> {
                 tags["bicycle_rental"] = answer.osmValue
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_shop/AddBikeRepairAvailability.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_shop/AddBikeRepairAvailability.kt
index fb009a9d0de..fb6de5ce16a 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_shop/AddBikeRepairAvailability.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_shop/AddBikeRepairAvailability.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bike_shop
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -36,7 +37,7 @@ class AddBikeRepairAvailability : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("service:bicycle:repair", answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_shop/AddSecondHandBicycleAvailability.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_shop/AddSecondHandBicycleAvailability.kt
index 3fe83d20714..a802005b5c4 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bike_shop/AddSecondHandBicycleAvailability.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bike_shop/AddSecondHandBicycleAvailability.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bike_shop
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -39,7 +40,7 @@ class AddSecondHandBicycleAvailability : OsmFilterQuestType<SecondHandBicycleAva
 
     override fun createForm() = AddSecondHandBicycleAvailabilityForm()
 
-    override fun applyAnswerTo(answer: SecondHandBicycleAvailability, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SecondHandBicycleAvailability, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         if (answer.osmValue == null) {
             tags.updateWithCheckDate("service:bicycle:retail", "no")
         } else {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/board_type/AddBoardType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/board_type/AddBoardType.kt
index 2ce7f0af545..4430e1e0baf 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/board_type/AddBoardType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/board_type/AddBoardType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.board_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -32,7 +33,7 @@ class AddBoardType : OsmFilterQuestType<BoardType>() {
 
     override fun createForm() = AddBoardTypeForm()
 
-    override fun applyAnswerTo(answer: BoardType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BoardType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         if (answer == BoardType.MAP) {
             tags["information"] = "map"
         } else {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bollard_type/AddBollardType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bollard_type/AddBollardType.kt
index 13119788b8d..1c06b8f34d2 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bollard_type/AddBollardType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bollard_type/AddBollardType.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.bollard_type
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -51,7 +52,7 @@ class AddBollardType : OsmElementQuestType<BollardTypeAnswer> {
 
     override fun createForm() = AddBollardTypeForm()
 
-    override fun applyAnswerTo(answer: BollardTypeAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BollardTypeAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is BollardType -> tags["bollard"] = answer.osmValue
             BarrierTypeIsNotBollard -> tags["barrier"] = "yes"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bridge_structure/AddBridgeStructure.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bridge_structure/AddBridgeStructure.kt
index 2d86cf365be..e75364cafe8 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bridge_structure/AddBridgeStructure.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bridge_structure/AddBridgeStructure.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bridge_structure
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BUILDING
 import de.westnordost.streetcomplete.osm.Tags
@@ -17,7 +18,7 @@ class AddBridgeStructure : OsmFilterQuestType<BridgeStructure>() {
 
     override fun createForm() = AddBridgeStructureForm()
 
-    override fun applyAnswerTo(answer: BridgeStructure, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BridgeStructure, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["bridge:structure"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/building_entrance/AddEntrance.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/building_entrance/AddEntrance.kt
index 0f696b83f60..e63dc3caf85 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/building_entrance/AddEntrance.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/building_entrance/AddEntrance.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.building_entrance
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -66,7 +67,7 @@ class AddEntrance : OsmElementQuestType<EntranceAnswer> {
 
     override fun createForm() = AddEntranceForm()
 
-    override fun applyAnswerTo(answer: EntranceAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: EntranceAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             DeadEnd -> tags["noexit"] = "yes"
             is EntranceExistsAnswer -> tags["entrance"] = answer.osmValue
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/building_entrance_reference/AddEntranceReference.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/building_entrance_reference/AddEntranceReference.kt
index c59ac088daa..43ce5dbcc7c 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/building_entrance_reference/AddEntranceReference.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/building_entrance_reference/AddEntranceReference.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.building_entrance_reference
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Node
@@ -87,7 +88,7 @@ class AddEntranceReference : OsmElementQuestType<EntranceAnswer> {
 
     override fun createForm() = AddEntranceReferenceForm()
 
-    override fun applyAnswerTo(answer: EntranceAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: EntranceAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is FlatRange -> {
                 tags["addr:flats"] = answer.flatRange
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/building_levels/AddBuildingLevels.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/building_levels/AddBuildingLevels.kt
index 18236e0bad9..90a41411782 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/building_levels/AddBuildingLevels.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/building_levels/AddBuildingLevels.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.building_levels
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BUILDING
 import de.westnordost.streetcomplete.osm.BUILDINGS_WITH_LEVELS
@@ -29,7 +30,7 @@ class AddBuildingLevels : OsmFilterQuestType<BuildingLevelsAnswer>() {
 
     override fun createForm() = AddBuildingLevelsForm()
 
-    override fun applyAnswerTo(answer: BuildingLevelsAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BuildingLevelsAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["building:levels"] = answer.levels.toString()
         answer.roofLevels?.let { tags["roof:levels"] = it.toString() }
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/building_type/AddBuildingType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/building_type/AddBuildingType.kt
index b7ced7a72e4..34666ec4d5d 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/building_type/AddBuildingType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/building_type/AddBuildingType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.building_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BUILDING
 import de.westnordost.streetcomplete.osm.Tags
@@ -39,7 +40,7 @@ class AddBuildingType : OsmFilterQuestType<BuildingType>() {
 
     override fun createForm() = AddBuildingTypeForm()
 
-    override fun applyAnswerTo(answer: BuildingType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BuildingType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         if (answer.osmKey == "man_made") {
             tags.remove("building")
             tags["man_made"] = answer.osmValue
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/building_underground/AddIsBuildingUnderground.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/building_underground/AddIsBuildingUnderground.kt
index 6c1d77788e4..329e5e6c387 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/building_underground/AddIsBuildingUnderground.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/building_underground/AddIsBuildingUnderground.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.building_underground
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BUILDING
 import de.westnordost.streetcomplete.osm.Tags
@@ -18,7 +19,7 @@ class AddIsBuildingUnderground : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["location"] = if (answer) "underground" else "surface"
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_bench/AddBenchStatusOnBusStop.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_bench/AddBenchStatusOnBusStop.kt
index a740b3dc8ec..0149bbede3a 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_bench/AddBenchStatusOnBusStop.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_bench/AddBenchStatusOnBusStop.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bus_stop_bench
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.osm.Tags
@@ -28,7 +29,7 @@ class AddBenchStatusOnBusStop : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("bench", answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_bin/AddBinStatusOnBusStop.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_bin/AddBinStatusOnBusStop.kt
index 1254b294b6f..06d3d95736b 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_bin/AddBinStatusOnBusStop.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_bin/AddBinStatusOnBusStop.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bus_stop_bin
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
 import de.westnordost.streetcomplete.osm.Tags
@@ -28,7 +29,7 @@ class AddBinStatusOnBusStop : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("bin", answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_lit/AddBusStopLit.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_lit/AddBusStopLit.kt
index 9fdad2e5910..262aa4000bb 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_lit/AddBusStopLit.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_lit/AddBusStopLit.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bus_stop_lit
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.osm.Tags
@@ -35,7 +36,7 @@ class AddBusStopLit : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("lit", answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopName.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopName.kt
index 9fe189b49c2..276948ef555 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopName.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopName.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bus_stop_name
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.quest.AllCountriesExcept
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
@@ -28,7 +29,7 @@ class AddBusStopName : OsmFilterQuestType<BusStopNameAnswer>() {
 
     override fun createForm() = AddBusStopNameForm()
 
-    override fun applyAnswerTo(answer: BusStopNameAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BusStopNameAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is NoBusStopName -> {
                 tags["name:signed"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_ref/AddBusStopRef.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_ref/AddBusStopRef.kt
index 5c2230a7fe0..a8b4db99407 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_ref/AddBusStopRef.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_ref/AddBusStopRef.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bus_stop_ref
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.quest.NoCountriesExcept
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
@@ -34,7 +35,7 @@ class AddBusStopRef : OsmFilterQuestType<BusStopRefAnswer>() {
 
     override fun createForm() = AddBusStopRefForm()
 
-    override fun applyAnswerTo(answer: BusStopRefAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BusStopRefAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is NoVisibleBusStopRef -> tags["ref:signed"] = "no"
             is BusStopRef ->          tags["ref"] = answer.ref
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_shelter/AddBusStopShelter.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_shelter/AddBusStopShelter.kt
index 9ba6f748e06..b9b6f844a42 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_shelter/AddBusStopShelter.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_shelter/AddBusStopShelter.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.bus_stop_shelter
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.osm.Tags
@@ -36,7 +37,7 @@ class AddBusStopShelter : OsmFilterQuestType<BusStopShelterAnswer>() {
 
     override fun createForm() = AddBusStopShelterForm()
 
-    override fun applyAnswerTo(answer: BusStopShelterAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: BusStopShelterAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             SHELTER -> tags.updateWithCheckDate("shelter", "yes")
             NO_SHELTER -> tags.updateWithCheckDate("shelter", "no")
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/camera_type/AddCameraType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/camera_type/AddCameraType.kt
index c547c38d547..6425f6abca7 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/camera_type/AddCameraType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/camera_type/AddCameraType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.camera_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -28,7 +29,7 @@ class AddCameraType : OsmFilterQuestType<CameraType>() {
 
     override fun createForm() = AddCameraTypeForm()
 
-    override fun applyAnswerTo(answer: CameraType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: CameraType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["camera:type"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampDrinkingWater.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampDrinkingWater.kt
index c9ad71d8a74..d28a71a83a4 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampDrinkingWater.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampDrinkingWater.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.camping
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -33,7 +34,7 @@ class AddCampDrinkingWater : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["drinking_water"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampPower.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampPower.kt
index d0ca21cda79..5bc15f206eb 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampPower.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampPower.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.camping
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -33,7 +34,7 @@ class AddCampPower : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["power_supply"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampShower.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampShower.kt
index 5da91447e3c..03f789844ec 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampShower.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampShower.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.camping
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -33,7 +34,7 @@ class AddCampShower : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["shower"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampType.kt
index b72bd4f65fb..c5a3042a998 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/camping/AddCampType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.camping
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -32,7 +33,7 @@ class AddCampType : OsmFilterQuestType<CampType>() {
 
     override fun createForm() = AddCampTypeForm()
 
-    override fun applyAnswerTo(answer: CampType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: CampType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             BACKCOUNTRY -> tags["backcountry"] = "yes"
             else -> {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/car_wash_type/AddCarWashType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/car_wash_type/AddCarWashType.kt
index 26e6688200f..38216c1e6d2 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/car_wash_type/AddCarWashType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/car_wash_type/AddCarWashType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.car_wash_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
 import de.westnordost.streetcomplete.osm.Tags
@@ -20,7 +21,7 @@ class AddCarWashType : OsmFilterQuestType<List<CarWashType>>() {
 
     override fun createForm() = AddCarWashTypeForm()
 
-    override fun applyAnswerTo(answer: List<CarWashType>, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: List<CarWashType>, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         val isAutomated = answer.contains(AUTOMATED)
         tags["automated"] = isAutomated.toYesNo()
 
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/charging_station_capacity/AddChargingStationCapacity.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/charging_station_capacity/AddChargingStationCapacity.kt
index 9587c7b0544..e80a78b9c3b 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/charging_station_capacity/AddChargingStationCapacity.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/charging_station_capacity/AddChargingStationCapacity.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.charging_station_capacity
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -30,7 +31,7 @@ class AddChargingStationCapacity : OsmFilterQuestType<Int>() {
 
     override fun createForm() = AddChargingStationCapacityForm()
 
-    override fun applyAnswerTo(answer: Int, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Int, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("capacity", answer.toString())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/charging_station_operator/AddChargingStationOperator.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/charging_station_operator/AddChargingStationOperator.kt
index 993a36e9c21..7f4f2a51d4e 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/charging_station_operator/AddChargingStationOperator.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/charging_station_operator/AddChargingStationOperator.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.charging_station_operator
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -28,7 +29,7 @@ class AddChargingStationOperator : OsmFilterQuestType<String>() {
 
     override fun createForm() = AddChargingStationOperatorForm()
 
-    override fun applyAnswerTo(answer: String, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: String, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["operator"] = answer
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/clothing_bin_operator/AddClothingBinOperator.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/clothing_bin_operator/AddClothingBinOperator.kt
index 34fbe68033d..30a3dc78b77 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/clothing_bin_operator/AddClothingBinOperator.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/clothing_bin_operator/AddClothingBinOperator.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.clothing_bin_operator
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -47,7 +48,7 @@ class AddClothingBinOperator : OsmElementQuestType<String> {
 
     override fun createForm() = AddClothingBinOperatorForm()
 
-    override fun applyAnswerTo(answer: String, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: String, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["operator"] = answer
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedBuildingConstruction.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedBuildingConstruction.kt
index 57a1d0a42ff..458764f71cf 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedBuildingConstruction.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedBuildingConstruction.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.construction
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BUILDING
 import de.westnordost.streetcomplete.osm.Tags
@@ -23,7 +24,7 @@ class MarkCompletedBuildingConstruction : OsmFilterQuestType<CompletedConstructi
 
     override fun createForm() = MarkCompletedConstructionForm()
 
-    override fun applyAnswerTo(answer: CompletedConstructionAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: CompletedConstructionAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is OpeningDateAnswer -> {
                 tags["opening_date"] = answer.date.toCheckDateString()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedHighwayConstruction.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedHighwayConstruction.kt
index 7f2549aecb9..b808e9b1d26 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedHighwayConstruction.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedHighwayConstruction.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.construction
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
 import de.westnordost.streetcomplete.osm.ALL_ROADS
@@ -41,7 +42,7 @@ class MarkCompletedHighwayConstruction : OsmFilterQuestType<CompletedConstructio
 
     override fun createForm() = MarkCompletedConstructionForm()
 
-    override fun applyAnswerTo(answer: CompletedConstructionAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: CompletedConstructionAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is OpeningDateAnswer -> {
                 tags["opening_date"] = answer.date.toCheckDateString()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/crossing/AddCrossing.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/crossing/AddCrossing.kt
index f3105939f94..50a189316b4 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/crossing/AddCrossing.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/crossing/AddCrossing.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.crossing
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Node
@@ -74,7 +75,7 @@ class AddCrossing : OsmElementQuestType<KerbHeight> {
 
     override fun createForm() = AddKerbHeightForm()
 
-    override fun applyAnswerTo(answer: KerbHeight, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: KerbHeight, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("kerb", answer.osmValue)
         /* So, we don't assume there is a crossing here for kerb=no and kerb=raised.
 
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/crossing_island/AddCrossingIsland.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/crossing_island/AddCrossingIsland.kt
index e8952f3cb29..1c3d847215f 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/crossing_island/AddCrossingIsland.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/crossing_island/AddCrossingIsland.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.crossing_island
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -56,7 +57,7 @@ class AddCrossingIsland : OsmElementQuestType<Boolean> {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["crossing:island"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/crossing_type/AddCrossingType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/crossing_type/AddCrossingType.kt
index 462fb62ac80..6de208aa73c 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/crossing_type/AddCrossingType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/crossing_type/AddCrossingType.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.crossing_type
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -63,7 +64,7 @@ class AddCrossingType : OsmElementQuestType<CrossingType> {
 
     override fun createForm() = AddCrossingTypeForm()
 
-    override fun applyAnswerTo(answer: CrossingType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: CrossingType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         val crossingValue = tags["crossing"]
         val isAndWasMarked = answer == CrossingType.MARKED && crossingValue in listOf("zebra", "marked", "uncontrolled")
         /* don't change the tag value of the synonyms for "marked" because it is something of a
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCycleway.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCycleway.kt
index 291d30516de..033f9b742e8 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCycleway.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCycleway.kt
@@ -8,6 +8,7 @@ import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpressio
 import de.westnordost.streetcomplete.data.meta.CountryInfo
 import de.westnordost.streetcomplete.data.meta.CountryInfos
 import de.westnordost.streetcomplete.data.meta.getByLocation
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
@@ -17,41 +18,29 @@ import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.
 import de.westnordost.streetcomplete.osm.ANYTHING_UNPAVED
 import de.westnordost.streetcomplete.osm.MAXSPEED_TYPE_KEYS
 import de.westnordost.streetcomplete.osm.Tags
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.ADVISORY_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.BUSWAY
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.DUAL_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.DUAL_TRACK
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.EXCLUSIVE_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.NONE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.NONE_NO_ONEWAY
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.PICTOGRAMS
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SEPARATE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SIDEWALK_EXPLICIT
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SUGGESTION_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.TRACK
 import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNSPECIFIED_LANE
 import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNSPECIFIED_SHARED_LANE
 import de.westnordost.streetcomplete.osm.cycleway.LeftAndRightCycleway
+import de.westnordost.streetcomplete.osm.cycleway.any
+import de.westnordost.streetcomplete.osm.cycleway.applyTo
 import de.westnordost.streetcomplete.osm.cycleway.createCyclewaySides
 import de.westnordost.streetcomplete.osm.cycleway.isAmbiguous
 import de.westnordost.streetcomplete.osm.estimateParkingOffRoadWidth
 import de.westnordost.streetcomplete.osm.estimateRoadwayWidth
 import de.westnordost.streetcomplete.osm.guessRoadwayWidth
-import de.westnordost.streetcomplete.osm.hasCheckDateForKey
-import de.westnordost.streetcomplete.osm.updateCheckDateForKey
 import de.westnordost.streetcomplete.util.math.isNearAndAligned
 import java.util.concurrent.FutureTask
 
 class AddCycleway(
     private val countryInfos: CountryInfos,
     private val countryBoundariesFuture: FutureTask<CountryBoundaries>,
-) : OsmElementQuestType<CyclewayAnswer> {
+) : OsmElementQuestType<LeftAndRightCycleway> {
 
     override val changesetComment = "Specify whether there are cycleways"
     override val wikiLink = "Key:cycleway"
     override val icon = R.drawable.ic_quest_bicycleway
     override val achievements = listOf(BICYCLIST)
+    override val defaultDisabledMessage = R.string.default_disabled_msg_overlay
 
     // See overview here: https://ent8r.github.io/blacklistr/?streetcomplete=cycleway/AddCycleway.kt
     // #749. sources:
@@ -160,229 +149,93 @@ class AddCycleway(
 
     override fun createForm() = AddCyclewayForm()
 
-    override fun applyAnswerTo(answer: CyclewayAnswer, tags: Tags, timestampEdited: Long) {
-        if (answer.left == answer.right) {
-            answer.left?.let { applyCyclewayAnswerTo(it.cycleway, Side.BOTH, 0, tags) }
-            deleteCyclewayAnswerIfExists(Side.LEFT, tags)
-            deleteCyclewayAnswerIfExists(Side.RIGHT, tags)
-        } else {
-            answer.left?.let { applyCyclewayAnswerTo(it.cycleway, Side.LEFT, it.dirInOneway, tags) }
-            answer.right?.let { applyCyclewayAnswerTo(it.cycleway, Side.RIGHT, it.dirInOneway, tags) }
-            deleteCyclewayAnswerIfExists(Side.BOTH, tags)
-        }
-        deleteCyclewayAnswerIfExists(null, tags)
-
-        applySidewalkAnswerTo(answer.left?.cycleway, answer.right?.cycleway, tags)
-
-        if (answer.isOnewayNotForCyclists) {
-            tags["oneway:bicycle"] = "no"
-        } else {
-            if (tags["oneway:bicycle"] == "no") {
-                tags.remove("oneway:bicycle")
-            }
-        }
-
-        // only set the check date if nothing was changed
-        if (!tags.hasChanges || tags.hasCheckDateForKey("cycleway")) {
-            tags.updateCheckDateForKey("cycleway")
-        }
-    }
-
-    /** Just add a sidewalk if we implicitly know from the answer that there is one */
-    private fun applySidewalkAnswerTo(cyclewayLeft: Cycleway?, cyclewayRight: Cycleway?, tags: Tags) {
-
-        /* only tag if we know the sidewalk value for both sides (because it is not possible in
-           OSM to specify the sidewalk value only for one side. sidewalk:right/left=yes is not
-           well established. */
-        if (cyclewayLeft?.isOnSidewalk == true && cyclewayRight?.isOnSidewalk == true) {
-            tags["sidewalk"] = "both"
-        }
-    }
-
-    private enum class Side(val value: String) {
-        LEFT("left"), RIGHT("right"), BOTH("both")
-    }
-
-    private fun applyCyclewayAnswerTo(cycleway: Cycleway, side: Side, dir: Int, tags: Tags) {
-        val directionValue = when {
-            dir > 0 -> "yes"
-            dir < 0 -> "-1"
-            else -> null
-        }
-
-        val cyclewayKey = "cycleway:" + side.value
-        when (cycleway) {
-            NONE, NONE_NO_ONEWAY -> {
-                tags[cyclewayKey] = "no"
-            }
-            EXCLUSIVE_LANE, ADVISORY_LANE, UNSPECIFIED_LANE -> {
-                tags[cyclewayKey] = "lane"
-                if (directionValue != null) {
-                    tags["$cyclewayKey:oneway"] = directionValue
-                }
-                if (cycleway == EXCLUSIVE_LANE) {
-                    tags["$cyclewayKey:lane"] = "exclusive"
-                } else if (cycleway == ADVISORY_LANE) {
-                    tags["$cyclewayKey:lane"] = "advisory"
-                }
-            }
-            TRACK -> {
-                tags[cyclewayKey] = "track"
-                if (directionValue != null) {
-                    tags["$cyclewayKey:oneway"] = directionValue
-                }
-                if (tags.containsKey("$cyclewayKey:segregated")) {
-                    tags["$cyclewayKey:segregated"] = "yes"
-                }
-            }
-            DUAL_TRACK -> {
-                tags[cyclewayKey] = "track"
-                tags["$cyclewayKey:oneway"] = "no"
-            }
-            DUAL_LANE -> {
-                tags[cyclewayKey] = "lane"
-                tags["$cyclewayKey:oneway"] = "no"
-                tags["$cyclewayKey:lane"] = "exclusive"
-            }
-            SIDEWALK_EXPLICIT -> {
-                // https://wiki.openstreetmap.org/wiki/File:Z240GemeinsamerGehundRadweg.jpeg
-                tags[cyclewayKey] = "track"
-                tags["$cyclewayKey:segregated"] = "no"
-            }
-            PICTOGRAMS -> {
-                tags[cyclewayKey] = "shared_lane"
-                tags["$cyclewayKey:lane"] = "pictogram"
-            }
-            SUGGESTION_LANE -> {
-                tags[cyclewayKey] = "shared_lane"
-                tags["$cyclewayKey:lane"] = "advisory"
-            }
-            BUSWAY -> {
-                tags[cyclewayKey] = "share_busway"
-            }
-            SEPARATE -> {
-                tags[cyclewayKey] = "separate"
-            }
-            else -> {
-                throw IllegalArgumentException("Invalid cycleway")
-            }
-        }
-
-        // clear previous cycleway:lane value
-        if (!cycleway.isLane) {
-            tags.remove("$cyclewayKey:lane")
-        }
-        // clear previous cycleway:oneway=no value (if not about to set a new value)
-        if (cycleway.isOneway && directionValue == null) {
-            if (tags["$cyclewayKey:oneway"] == "no") {
-                tags.remove("$cyclewayKey:oneway")
-            }
-        }
-        // clear previous cycleway:segregated=no value
-        if (cycleway != SIDEWALK_EXPLICIT && cycleway != TRACK) {
-            if (tags["$cyclewayKey:segregated"] == "no") {
-                tags.remove("$cyclewayKey:segregated")
-            }
-        }
+    override fun applyAnswerTo(answer: LeftAndRightCycleway, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
+        val countryInfo = countryInfos.getByLocation(
+            countryBoundariesFuture.get(),
+            geometry.center.longitude,
+            geometry.center.latitude
+        )
+        answer.applyTo(tags, countryInfo.isLeftHandTraffic)
     }
+}
 
-    /** clear previous answers for the given side */
-    private fun deleteCyclewayAnswerIfExists(side: Side?, tags: Tags) {
-        val sideVal = if (side == null) "" else ":" + side.value
-        val cyclewayKey = "cycleway$sideVal"
-
-        // only things are cleared that are set by this quest
-        // for example cycleway:surface should only be cleared by a cycleway surface quest etc.
-        tags.remove(cyclewayKey)
-        tags.remove("$cyclewayKey:lane")
-        tags.remove("$cyclewayKey:oneway")
-        tags.remove("$cyclewayKey:segregated")
-        tags.remove("sidewalk$sideVal:bicycle")
-    }
-
-    companion object {
-
-        /* Excluded is
-          - anything explicitly tagged as no bicycles or having to use separately mapped sidepath
-          - if not already tagged with a cycleway: streets with low speed or that are not paved, as
-            they are very unlikely to have cycleway infrastructure
-                - for highway=residential without speed limit tagged assume low speed
-          - if not already tagged, roads that are close (15m) to foot or cycleways (see #718)
-          - if already tagged, if not older than 4 years or if the cycleway tag uses some unknown value
-        */
-
-        // streets that may have cycleway tagging
-        private val roadsFilter by lazy { """
-            ways with
-              highway ~ primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified|residential|service
-              and area != yes
-              and motorroad != yes
-              and bicycle_road != yes
-              and cyclestreet != yes
-              and bicycle != no
-              and bicycle != designated
-              and access !~ private|no
-              and bicycle != use_sidepath
-              and bicycle:backward != use_sidepath
-              and bicycle:forward != use_sidepath
-              and sidewalk != separate
-        """.toElementFilterExpression() }
-
-        // streets that do not have cycleway tagging yet
-        private val untaggedRoadsFilter by lazy { """
-            ways with (
-                highway ~ primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified
-                or highway = residential and (maxspeed > 33 or $notIn30ZoneOrLess)
-              )
-              and !cycleway
-              and !cycleway:left
-              and !cycleway:right
-              and !cycleway:both
-              and !sidewalk:bicycle
-              and !sidewalk:left:bicycle
-              and !sidewalk:right:bicycle
-              and !sidewalk:both:bicycle
-              and (
-                !maxspeed
-                or maxspeed > 20
-                or $notIn30ZoneOrLess
-              )
-              and surface !~ ${ANYTHING_UNPAVED.joinToString("|")}
-        """.toElementFilterExpression() }
-
-        private val maybeSeparatelyMappedCyclewaysFilter by lazy { """
-            ways with highway ~ path|footway|cycleway|construction
-        """.toElementFilterExpression() }
-        // highway=construction included, as situation often changes during and after construction
-
-        private val notIn30ZoneOrLess = MAXSPEED_TYPE_KEYS.joinToString(" or ") {
-            """$it and $it !~ ".*zone:?([1-9]|[1-2][0-9]|30)""""
-        }
-
-        private val olderThan4Years = TagOlderThan("cycleway", RelativeDate(-(365 * 4).toFloat()))
+/* Excluded is
+  - anything explicitly tagged as no bicycles or having to use separately mapped sidepath
+  - if not already tagged with a cycleway: streets with low speed or that are not paved, as
+    they are very unlikely to have cycleway infrastructure
+        - for highway=residential without speed limit tagged assume low speed
+  - if not already tagged, roads that are close (15m) to foot or cycleways (see #718)
+  - if already tagged, if not older than 4 years or if the cycleway tag uses some unknown value
+*/
+
+// streets that may have cycleway tagging
+private val roadsFilter by lazy { """
+    ways with
+      highway ~ primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified|residential|service
+      and area != yes
+      and motorroad != yes
+      and expressway != yes
+      and bicycle_road != yes
+      and cyclestreet != yes
+      and bicycle != no
+      and bicycle != designated
+      and access !~ private|no
+      and bicycle != use_sidepath
+      and bicycle:backward != use_sidepath
+      and bicycle:forward != use_sidepath
+      and sidewalk != separate
+""".toElementFilterExpression() }
+
+// streets that do not have cycleway tagging yet
+private val untaggedRoadsFilter by lazy { """
+    ways with (
+        highway ~ primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified
+        or highway = residential and (maxspeed > 33 or $notIn30ZoneOrLess)
+      )
+      and !cycleway
+      and !cycleway:left
+      and !cycleway:right
+      and !cycleway:both
+      and !sidewalk:bicycle
+      and !sidewalk:left:bicycle
+      and !sidewalk:right:bicycle
+      and !sidewalk:both:bicycle
+      and (
+        !maxspeed
+        or maxspeed > 20
+        or $notIn30ZoneOrLess
+      )
+      and surface !~ ${ANYTHING_UNPAVED.joinToString("|")}
+""".toElementFilterExpression() }
+
+private val maybeSeparatelyMappedCyclewaysFilter by lazy { """
+    ways with highway ~ path|footway|cycleway|construction
+""".toElementFilterExpression() }
+// highway=construction included, as situation often changes during and after construction
+
+private val notIn30ZoneOrLess = MAXSPEED_TYPE_KEYS.joinToString(" or ") {
+    """$it and $it !~ ".*zone:?([1-9]|[1-2][0-9]|30)""""
+}
 
-        private fun Element.hasOldInvalidOrAmbiguousCyclewayTags(countryInfo: CountryInfo?): Boolean? {
-            val sides = createCyclewaySides(tags, false)
-            // has no cycleway tagging
-            if (sides == null) return false
-            // any cycleway tagging is not known: don't mess with that
-            if (sides.any { it.isUnknown }) return false
-            // has any invalid cycleway tags
-            if (sides.any { it.isInvalid }) return true
-            // or it is older than x years
-            if (olderThan4Years.matches(this)) return true
-            // has any ambiguous cycleway tags
-            if (countryInfo != null) {
-                if (sides.any { it.isAmbiguous(countryInfo) }) return true
-            } else {
-                if (sides.any { it == UNSPECIFIED_SHARED_LANE }) return true
-                // for this, a countryCode is necessary, thus return null if no country code is available
-                if (sides.any { it == UNSPECIFIED_LANE }) return null
-            }
-            return false
-        }
+private val olderThan4Years = TagOlderThan("cycleway", RelativeDate(-(365 * 4).toFloat()))
+
+private fun Element.hasOldInvalidOrAmbiguousCyclewayTags(countryInfo: CountryInfo?): Boolean? {
+    val sides = createCyclewaySides(tags, false)
+    // has no cycleway tagging
+    if (sides == null) return false
+    // any cycleway tagging is not known: don't mess with that
+    if (sides.any { it.cycleway.isUnknown }) return false
+    // has any invalid cycleway tags
+    if (sides.any { it.cycleway.isInvalid }) return true
+    // or it is older than x years
+    if (olderThan4Years.matches(this)) return true
+    // has any ambiguous cycleway tags
+    if (countryInfo != null) {
+        if (sides.any { it.cycleway.isAmbiguous(countryInfo) }) return true
+    } else {
+        if (sides.any { it.cycleway == UNSPECIFIED_SHARED_LANE }) return true
+        // for this, a countryCode is necessary, thus return null if no country code is available
+        if (sides.any { it.cycleway == UNSPECIFIED_LANE }) return null
     }
+    return false
 }
-
-private fun LeftAndRightCycleway.any(block: (cycleway: Cycleway) -> Boolean): Boolean =
-    left?.let(block) == true || right?.let(block) == true
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayForm.kt
index 402240ba60b..20b10180487 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayForm.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayForm.kt
@@ -6,22 +6,31 @@ import androidx.appcompat.app.AlertDialog
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
 import de.westnordost.streetcomplete.osm.cycleway.Cycleway
+import de.westnordost.streetcomplete.osm.cycleway.CyclewayAndDirection
+import de.westnordost.streetcomplete.osm.cycleway.LeftAndRightCycleway
+import de.westnordost.streetcomplete.osm.cycleway.asDialogItem
+import de.westnordost.streetcomplete.osm.cycleway.asStreetSideItem
 import de.westnordost.streetcomplete.osm.cycleway.createCyclewaySides
-import de.westnordost.streetcomplete.osm.cycleway.getSelectableCyclewaysInCountry
-import de.westnordost.streetcomplete.osm.cycleway.isAmbiguous
-import de.westnordost.streetcomplete.osm.isForwardOneway
-import de.westnordost.streetcomplete.osm.isNotOnewayForCyclists
+import de.westnordost.streetcomplete.osm.cycleway.getSelectableCycleways
+import de.westnordost.streetcomplete.osm.cycleway.wasNoOnewayForCyclistsButNowItIs
+import de.westnordost.streetcomplete.osm.cycleway.selectableOrNullValues
 import de.westnordost.streetcomplete.osm.isOneway
 import de.westnordost.streetcomplete.osm.isReversedOneway
 import de.westnordost.streetcomplete.quests.AStreetSideSelectForm
 import de.westnordost.streetcomplete.quests.AnswerItem
+import de.westnordost.streetcomplete.quests.IAnswerItem
+import de.westnordost.streetcomplete.util.ktx.toast
 import de.westnordost.streetcomplete.view.controller.StreetSideDisplayItem
+import de.westnordost.streetcomplete.view.controller.StreetSideSelectWithLastAnswerButtonViewController
 import de.westnordost.streetcomplete.view.controller.StreetSideSelectWithLastAnswerButtonViewController.Sides.BOTH
 import de.westnordost.streetcomplete.view.controller.StreetSideSelectWithLastAnswerButtonViewController.Sides.LEFT
 import de.westnordost.streetcomplete.view.controller.StreetSideSelectWithLastAnswerButtonViewController.Sides.RIGHT
 import de.westnordost.streetcomplete.view.image_select.ImageListPickerDialog
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
 
-class AddCyclewayForm : AStreetSideSelectForm<Cycleway, CyclewayAnswer>() {
+class AddCyclewayForm : AStreetSideSelectForm<CyclewayAndDirection, LeftAndRightCycleway>() {
 
     override val buttonPanelAnswers get() =
         if (isDisplayingPrevious) listOf(
@@ -30,15 +39,11 @@ class AddCyclewayForm : AStreetSideSelectForm<Cycleway, CyclewayAnswer>() {
         )
         else emptyList()
 
-    override val otherAnswers: List<AnswerItem> get() {
-        val isNoRoundabout = element.tags["junction"] != "roundabout" && element.tags["junction"] != "circular"
-        val result = mutableListOf<AnswerItem>()
-        if (streetSideSelect.showSides != BOTH && isNoRoundabout) {
-            result.add(AnswerItem(R.string.quest_cycleway_answer_contraflow_cycleway) { streetSideSelect.showSides = BOTH })
-        }
-        result.add(AnswerItem(R.string.quest_cycleway_answer_no_bicycle_infrastructure) { noCyclewayHereHint() })
-        return result
-    }
+    override val otherAnswers: List<IAnswerItem> get() = listOfNotNull(
+        createShowBothSidesAnswer(),
+        AnswerItem(R.string.quest_cycleway_answer_no_bicycle_infrastructure, ::noCyclewayHereHint),
+        AnswerItem(R.string.cycleway_reverse_direction, ::selectReverseCyclewayDirection)
+    )
 
     private fun noCyclewayHereHint() {
         activity?.let { AlertDialog.Builder(it)
@@ -49,27 +54,22 @@ class AddCyclewayForm : AStreetSideSelectForm<Cycleway, CyclewayAnswer>() {
         }
     }
 
-    private val likelyNoBicycleContraflow = """
-        ways with oneway:bicycle != no and (
-            oneway ~ yes|-1 and highway ~ primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified
-            or junction ~ roundabout|circular
-        )
-    """.toElementFilterExpression()
+    private var reverseDirection: Boolean = false
 
     /** returns whether the side that goes into the opposite direction of the driving direction of a
      * one-way is on the right side of the way */
     private val isReverseSideRight get() = isReversedOneway xor isLeftHandTraffic
 
     private val isOneway get() = isOneway(element.tags)
-
-    private val isForwardOneway get() = isForwardOneway(element.tags)
     private val isReversedOneway get() = isReversedOneway(element.tags)
 
     // just a shortcut
     private val isLeftHandTraffic get() = countryInfo.isLeftHandTraffic
 
     private fun isContraflowInOneway(isRight: Boolean): Boolean =
-        isOneway && (isReverseSideRight xor !isRight)
+        isOneway && (isReverseSideRight == isRight)
+
+    /* ---------------------------------------- lifecycle --------------------------------------- */
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
@@ -79,106 +79,101 @@ class AddCyclewayForm : AStreetSideSelectForm<Cycleway, CyclewayAnswer>() {
     }
 
     private fun initStateFromTags() {
-        val sides = createCyclewaySides(element.tags, isLeftHandTraffic)
-        val left = sides?.left?.takeIf { !it.isAmbiguous(countryInfo) && !it.isInvalid && !it.isUnknown }
-        val right = sides?.right?.takeIf { !it.isAmbiguous(countryInfo) && !it.isInvalid && !it.isUnknown }
-        val bothSidesWereDefinedBefore = sides?.left != null && sides.right != null
-        val bicycleTrafficOnBothSidesIsLikely = !likelyNoBicycleContraflow.matches(element)
+        val cycleways = createCyclewaySides(element.tags, isLeftHandTraffic)?.selectableOrNullValues(countryInfo)
 
-        streetSideSelect.showSides = when {
-            bothSidesWereDefinedBefore || bicycleTrafficOnBothSidesIsLikely -> BOTH
-            isLeftHandTraffic -> LEFT
-            else -> RIGHT
-        }
-        val leftItem = left?.asStreetSideItem(countryInfo, isContraflowInOneway(false))
+        streetSideSelect.showSides = getInitiallyShownSides(cycleways)
+
+        val leftItem = cycleways?.left?.asStreetSideItem(false, isContraflowInOneway(false), countryInfo)
         streetSideSelect.setPuzzleSide(leftItem, false)
 
-        val rightItem = right?.asStreetSideItem(countryInfo, isContraflowInOneway(true))
+        val rightItem = cycleways?.right?.asStreetSideItem(true, isContraflowInOneway(true), countryInfo)
         streetSideSelect.setPuzzleSide(rightItem, true)
 
         // only show as re-survey (yes/no button) if the previous tagging was complete
         isDisplayingPrevious = streetSideSelect.isComplete
     }
 
-    /* ---------------------------------- selection dialog -------------------------------------- */
+    /* --------------------------------- showing only one side ---------------------------------- */
 
-    override fun onClickSide(isRight: Boolean) {
-        val isContraflowInOneway = isContraflowInOneway(isRight)
-        val dialogItems = getSelectableCycleways(isRight)
-            .map { it.asDialogItem(requireContext(), countryInfo, isContraflowInOneway) }
+    private fun createShowBothSidesAnswer(): IAnswerItem? {
+        val isNoRoundabout = element.tags["junction"] != "roundabout" && element.tags["junction"] != "circular"
+        return if (streetSideSelect.showSides != BOTH && isNoRoundabout) {
+            AnswerItem(R.string.quest_cycleway_answer_contraflow_cycleway) { streetSideSelect.showSides = BOTH }
+        } else null
+    }
 
-        ImageListPickerDialog(requireContext(), dialogItems, R.layout.labeled_icon_button_cell, 2) { item ->
-            val streetSideItem = item.value!!.asStreetSideItem(countryInfo, isContraflowInOneway)
-            streetSideSelect.replacePuzzleSide(streetSideItem, isRight)
-        }.show()
+    private fun getInitiallyShownSides(cycleways: LeftAndRightCycleway?): StreetSideSelectWithLastAnswerButtonViewController.Sides {
+        val bothSidesWereDefinedBefore = cycleways?.left != null && cycleways.right != null
+        val bicycleTrafficOnBothSidesIsLikely = !likelyNoBicycleContraflow.matches(element)
+
+        return when {
+            bothSidesWereDefinedBefore || bicycleTrafficOnBothSidesIsLikely -> BOTH
+            isLeftHandTraffic -> LEFT
+            else -> RIGHT
+        }
     }
 
-    private fun getSelectableCycleways(isRight: Boolean): List<Cycleway> {
-        val values = getSelectableCyclewaysInCountry(countryInfo).toMutableList()
-        // different wording for a contraflow lane that is marked like a "shared" lane (just bicycle pictogram)
-        if (isOneway && isReverseSideRight == isRight) {
-            values.remove(Cycleway.PICTOGRAMS)
-            values.add(values.indexOf(Cycleway.NONE) + 1, Cycleway.NONE_NO_ONEWAY)
+    private val likelyNoBicycleContraflow = """
+        ways with oneway:bicycle != no and (
+            oneway ~ yes|-1 and highway ~ primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified
+            or junction ~ roundabout|circular
+        )
+    """.toElementFilterExpression()
+
+    /* ------------------------------ reverse cycleway direction -------------------------------- */
+
+    private fun selectReverseCyclewayDirection() {
+        confirmSelectReverseCyclewayDirection {
+            reverseDirection = true
+            context?.toast(R.string.cycleway_reverse_direction_toast)
         }
-        return values
     }
 
-    override fun serialize(item: Cycleway) =  item.name
-    override fun deserialize(str: String) = Cycleway.valueOf(str)
-    override fun asStreetSideItem(item: Cycleway, isRight: Boolean): StreetSideDisplayItem<Cycleway> {
+    private fun confirmSelectReverseCyclewayDirection(callback: () -> Unit) {
+        AlertDialog.Builder(requireContext())
+            .setTitle(R.string.quest_generic_confirmation_title)
+            .setMessage(R.string.cycleway_reverse_direction_warning)
+            .setPositiveButton(R.string.quest_generic_confirmation_yes) { _, _ -> callback() }
+            .setNegativeButton(R.string.quest_generic_confirmation_no, null)
+            .show()
+    }
+
+    private fun reverseCyclewayDirection(isRight: Boolean) {
         val isContraflowInOneway = isContraflowInOneway(isRight)
-        // NONE_NO_ONEWAY is displayed as simply NONE if not in contraflow because the former makes
-        // only really sense in contraflow. This can only happen when applying the side(s) via the
-        // last answer button
-        val item2 = if (item == Cycleway.NONE_NO_ONEWAY && !isContraflowInOneway) Cycleway.NONE else item
-        return item2.asStreetSideItem(countryInfo, isContraflowInOneway)
+        reverseDirection = false
+        val value = streetSideSelect.getPuzzleSide(isRight)?.value ?: return
+        val newValue = value.copy(direction = value.direction.reverse())
+        val newItem = newValue.asStreetSideItem(isRight, isContraflowInOneway, countryInfo)
+        streetSideSelect.replacePuzzleSide(newItem, isRight)
     }
 
-    /* --------------------------------------- apply answer ------------------------------------- */
+    /* --------------------------------- select & apply answer ---------------------------------- */
 
-    override fun onClickOk() {
-        val leftSide = streetSideSelect.left?.value
-        val rightSide = streetSideSelect.right?.value
-
-        // a cycleway that goes into opposite direction of a oneway street needs special tagging
-        // as oneway:bicycle=* tag will differ from oneway=*
-        // there is no need to tag cases where oneway:bicycle=* would merely repeat oneway=*
-        var leftSideDir = 0
-        var rightSideDir = 0
-        var isOnewayNotForCyclists = false
-        if (isOneway && leftSide != null && rightSide != null) {
-            // if the road is oneway=-1, a cycleway that goes opposite to it would be cycleway:oneway=yes
-            val reverseDir = if (isReversedOneway) 1 else -1
-
-            if (isReverseSideRight) {
-                if (rightSide.isSingleTrackOrLane()) {
-                    rightSideDir = reverseDir
-                }
-            } else {
-                if (leftSide.isSingleTrackOrLane()) {
-                    leftSideDir = reverseDir
-                }
-            }
-
-            isOnewayNotForCyclists = leftSide.isDualTrackOrLane() || rightSide.isDualTrackOrLane()
-                || (if (isReverseSideRight) rightSide else leftSide) !== Cycleway.NONE
+    override fun onClickSide(isRight: Boolean) {
+        if (reverseDirection) {
+            reverseCyclewayDirection(isRight)
+        } else {
+            selectCycleway(isRight)
         }
+    }
 
-        val answer = CyclewayAnswer(
-            left = leftSide?.let { CyclewaySide(it, leftSideDir) },
-            right = rightSide?.let { CyclewaySide(it, rightSideDir) },
-            isOnewayNotForCyclists = isOnewayNotForCyclists
-        )
+    private fun selectCycleway(isRight: Boolean) {
+        val isContraflowInOneway = isContraflowInOneway(isRight)
+        val dialogItems = getSelectableCycleways(countryInfo, element.tags, isRight)
+            .map { it.asDialogItem(isRight, isContraflowInOneway, requireContext(), countryInfo, ) }
 
-        val wasOnewayNotForCyclists = isOneway && isNotOnewayForCyclists(element.tags, isLeftHandTraffic)
-        if (!isOnewayNotForCyclists && wasOnewayNotForCyclists) {
-            confirmNotOnewayForCyclists {
-                applyAnswer(answer)
-                streetSideSelect.saveLastSelection()
-            }
+        ImageListPickerDialog(requireContext(), dialogItems, R.layout.labeled_icon_button_cell, 2) { item ->
+            val streetSideItem = item.value!!.asStreetSideItem(isRight, isContraflowInOneway, countryInfo)
+            streetSideSelect.replacePuzzleSide(streetSideItem, isRight)
+        }.show()
+    }
+
+    override fun onClickOk() {
+        val cycleways = LeftAndRightCycleway(streetSideSelect.left?.value, streetSideSelect.right?.value)
+        if (cycleways.wasNoOnewayForCyclistsButNowItIs(element.tags, isLeftHandTraffic)) {
+            confirmNotOnewayForCyclists { saveAndApplyCycleway(cycleways) }
         } else {
-            applyAnswer(answer)
-            streetSideSelect.saveLastSelection()
+            saveAndApplyCycleway(cycleways)
         }
     }
 
@@ -189,10 +184,22 @@ class AddCyclewayForm : AStreetSideSelectForm<Cycleway, CyclewayAnswer>() {
             .setNegativeButton(R.string.quest_generic_confirmation_no, null)
             .show()
     }
-}
 
-private fun Cycleway.isSingleTrackOrLane() =
-    this === Cycleway.TRACK || this === Cycleway.EXCLUSIVE_LANE
+    private fun saveAndApplyCycleway(cycleways: LeftAndRightCycleway) {
+        streetSideSelect.saveLastSelection()
+        applyAnswer(cycleways)
+    }
+
+    /* --------------------------------- AStreetSideSelectForm ---------------------------------- */
 
-private fun Cycleway.isDualTrackOrLane() =
-    this === Cycleway.DUAL_TRACK || this === Cycleway.DUAL_LANE
+    override fun serialize(item: CyclewayAndDirection) = Json.encodeToString(item)
+    override fun deserialize(str: String) = Json.decodeFromString<CyclewayAndDirection>(str)
+    override fun asStreetSideItem(item: CyclewayAndDirection, isRight: Boolean): StreetSideDisplayItem<CyclewayAndDirection> {
+        val isContraflowInOneway = isContraflowInOneway(isRight)
+        // NONE_NO_ONEWAY is displayed as simply NONE if not in contraflow because the former makes
+        // only really sense in contraflow. This can only happen when applying the side(s) via the
+        // last answer button
+        val item2 = if (item.cycleway == Cycleway.NONE_NO_ONEWAY && !isContraflowInOneway) item.copy(cycleway = Cycleway.NONE) else item
+        return item2.asStreetSideItem(isRight, isContraflowInOneway, countryInfo)
+    }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/CyclewayAnswer.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/CyclewayAnswer.kt
deleted file mode 100644
index 6e2dec09955..00000000000
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/CyclewayAnswer.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package de.westnordost.streetcomplete.quests.cycleway
-
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway
-
-data class CyclewayAnswer(
-    val left: CyclewaySide?,
-    val right: CyclewaySide?,
-    val isOnewayNotForCyclists: Boolean = false
-)
-
-data class CyclewaySide(val cycleway: Cycleway, val dirInOneway: Int = 0)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/CyclewayItem.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/CyclewayItem.kt
deleted file mode 100644
index 81ced59ca1e..00000000000
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/CyclewayItem.kt
+++ /dev/null
@@ -1,139 +0,0 @@
-package de.westnordost.streetcomplete.quests.cycleway
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.drawable.Drawable
-import de.westnordost.streetcomplete.R
-import de.westnordost.streetcomplete.data.meta.CountryInfo
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.ADVISORY_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.BUSWAY
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.DUAL_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.DUAL_TRACK
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.EXCLUSIVE_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.NONE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.NONE_NO_ONEWAY
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.PICTOGRAMS
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SEPARATE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SIDEWALK_EXPLICIT
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SUGGESTION_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.TRACK
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNSPECIFIED_LANE
-import de.westnordost.streetcomplete.util.ktx.getAdvisoryCycleLaneResId
-import de.westnordost.streetcomplete.util.ktx.getDualCycleLaneResId
-import de.westnordost.streetcomplete.util.ktx.getExclusiveCycleLaneResId
-import de.westnordost.streetcomplete.util.ktx.getPictogramCycleLaneResId
-import de.westnordost.streetcomplete.util.ktx.noEntrySignDrawableResId
-import de.westnordost.streetcomplete.view.DrawableImage
-import de.westnordost.streetcomplete.view.DrawableWrapper
-import de.westnordost.streetcomplete.view.Image
-import de.westnordost.streetcomplete.view.ResImage
-import de.westnordost.streetcomplete.view.ResText
-import de.westnordost.streetcomplete.view.controller.StreetSideItem
-import de.westnordost.streetcomplete.view.image_select.Item2
-
-fun Cycleway.asDialogItem(context: Context, countryInfo: CountryInfo, isContraflowInOneway: Boolean) =
-    Item2(
-        this,
-        getDialogIcon(context, countryInfo),
-        ResText(getTitleResId(isContraflowInOneway))
-    )
-
-fun Cycleway.asStreetSideItem(
-    countryInfo: CountryInfo,
-    isContraflowInOneway: Boolean
-) =
-    StreetSideItem(
-        this,
-        getIconResId(countryInfo),
-        getTitleResId(isContraflowInOneway),
-        getDialogIconResId(countryInfo),
-        getFloatingIconResId(isContraflowInOneway, countryInfo.noEntrySignDrawableResId)
-    )
-
-private fun Cycleway.getDialogIcon(context: Context, countryInfo: CountryInfo): Image {
-    val id = getDialogIconResId(countryInfo)
-    return if (countryInfo.isLeftHandTraffic) {
-        DrawableImage(Rotate180Degrees(context.getDrawable(id)!!))
-    } else {
-        ResImage(id)
-    }
-}
-
-private fun Cycleway.getDialogIconResId(countryInfo: CountryInfo): Int =
-    when (this) {
-        NONE ->     R.drawable.ic_cycleway_none_in_selection
-        SEPARATE -> R.drawable.ic_cycleway_separate
-        else ->     getIconResId(countryInfo)
-    }
-
-private class Rotate180Degrees(drawable: Drawable) : DrawableWrapper(drawable) {
-    override fun draw(canvas: Canvas) {
-        canvas.scale(-1f, -1f, bounds.width() / 2f, bounds.height() / 2f)
-        drawable.bounds = bounds
-        drawable.draw(canvas)
-    }
-}
-
-private fun Cycleway.getFloatingIconResId(isContraflowInOneway: Boolean, noEntrySignDrawableResId: Int): Int? = when (this) {
-    NONE -> if (isContraflowInOneway) noEntrySignDrawableResId else null
-    SEPARATE -> R.drawable.ic_sidewalk_floating_separate
-    else -> null
-}
-
-private fun Cycleway.getIconResId(countryInfo: CountryInfo): Int =
-    if (countryInfo.isLeftHandTraffic) getLeftHandTrafficIconResId(countryInfo) else getRightHandTrafficIconResId(countryInfo)
-
-private fun Cycleway.getRightHandTrafficIconResId(countryInfo: CountryInfo): Int = when (this) {
-    UNSPECIFIED_LANE ->  countryInfo.getExclusiveCycleLaneResId(false)
-    EXCLUSIVE_LANE ->    countryInfo.getExclusiveCycleLaneResId(false)
-    ADVISORY_LANE ->     countryInfo.getAdvisoryCycleLaneResId(false)
-    SUGGESTION_LANE ->   countryInfo.getAdvisoryCycleLaneResId(false)
-    TRACK ->             R.drawable.ic_cycleway_track
-    NONE ->              R.drawable.ic_cycleway_none
-    NONE_NO_ONEWAY ->    R.drawable.ic_cycleway_none_no_oneway
-    PICTOGRAMS ->        countryInfo.getPictogramCycleLaneResId(false)
-    SIDEWALK_EXPLICIT -> R.drawable.ic_cycleway_sidewalk_explicit
-    DUAL_LANE ->         countryInfo.getDualCycleLaneResId(false)
-    DUAL_TRACK ->        R.drawable.ic_cycleway_track_dual
-    BUSWAY ->            R.drawable.ic_cycleway_bus_lane
-    SEPARATE ->          R.drawable.ic_cycleway_none
-    else -> 0
-}
-
-private fun Cycleway.getLeftHandTrafficIconResId(countryInfo: CountryInfo): Int = when (this) {
-    UNSPECIFIED_LANE ->  countryInfo.getExclusiveCycleLaneResId(true)
-    EXCLUSIVE_LANE ->    countryInfo.getExclusiveCycleLaneResId(true)
-    ADVISORY_LANE ->     countryInfo.getAdvisoryCycleLaneResId(true)
-    SUGGESTION_LANE ->   countryInfo.getAdvisoryCycleLaneResId(true)
-    TRACK ->             R.drawable.ic_cycleway_track_l
-    NONE ->              R.drawable.ic_cycleway_none
-    NONE_NO_ONEWAY ->    R.drawable.ic_cycleway_none_no_oneway_l
-    PICTOGRAMS ->        countryInfo.getPictogramCycleLaneResId(true)
-    SIDEWALK_EXPLICIT -> R.drawable.ic_cycleway_sidewalk_explicit_l
-    DUAL_LANE ->         countryInfo.getDualCycleLaneResId(true)
-    DUAL_TRACK ->        R.drawable.ic_cycleway_track_dual_l
-    BUSWAY ->            R.drawable.ic_cycleway_bus_lane_l
-    SEPARATE ->          R.drawable.ic_cycleway_none
-    else -> 0
-}
-
-private fun Cycleway.getTitleResId(isContraflowInOneway: Boolean): Int = when (this) {
-    UNSPECIFIED_LANE ->  R.string.quest_cycleway_value_lane
-    EXCLUSIVE_LANE ->    R.string.quest_cycleway_value_lane
-    ADVISORY_LANE ->     R.string.quest_cycleway_value_advisory_lane
-    SUGGESTION_LANE ->   R.string.quest_cycleway_value_advisory_lane
-    TRACK ->             R.string.quest_cycleway_value_track
-    NONE -> {
-        if (isContraflowInOneway) R.string.quest_cycleway_value_none_and_oneway
-        else                      R.string.quest_cycleway_value_none
-    }
-    NONE_NO_ONEWAY ->    R.string.quest_cycleway_value_none_but_no_oneway
-    PICTOGRAMS ->        R.string.quest_cycleway_value_shared
-    SIDEWALK_EXPLICIT -> R.string.quest_cycleway_value_sidewalk
-    DUAL_LANE ->         R.string.quest_cycleway_value_lane_dual
-    DUAL_TRACK ->        R.string.quest_cycleway_value_track_dual
-    BUSWAY ->            R.string.quest_cycleway_value_bus_lane
-    SEPARATE ->          R.string.quest_cycleway_value_separate
-    else -> 0
-}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/defibrillator/AddIsDefibrillatorIndoor.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/defibrillator/AddIsDefibrillatorIndoor.kt
index 4d36d1c139b..0842d373eb6 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/defibrillator/AddIsDefibrillatorIndoor.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/defibrillator/AddIsDefibrillatorIndoor.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.defibrillator
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -30,7 +31,7 @@ class AddIsDefibrillatorIndoor : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["indoor"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddHalal.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddHalal.kt
index 7ac67f6ef64..625807024d2 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddHalal.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddHalal.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.diet_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -38,7 +39,7 @@ class AddHalal : OsmFilterQuestType<DietAvailabilityAnswer>() {
 
     override fun createForm() = AddDietTypeForm.create(R.string.quest_dietType_explanation_halal)
 
-    override fun applyAnswerTo(answer: DietAvailabilityAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: DietAvailabilityAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is DietAvailability -> tags.updateWithCheckDate("diet:halal", answer.osmValue)
             NoFood -> tags["food"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddKosher.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddKosher.kt
index 52e38781d7f..7a06264a412 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddKosher.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddKosher.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.diet_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -38,7 +39,7 @@ class AddKosher : OsmFilterQuestType<DietAvailabilityAnswer>() {
 
     override fun createForm() = AddDietTypeForm.create(R.string.quest_dietType_explanation_kosher)
 
-    override fun applyAnswerTo(answer: DietAvailabilityAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: DietAvailabilityAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is DietAvailability -> tags.updateWithCheckDate("diet:kosher", answer.osmValue)
             NoFood -> tags["food"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddVegan.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddVegan.kt
index f4ca26104bc..a2e4edaa8e0 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddVegan.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddVegan.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.diet_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -42,7 +43,7 @@ class AddVegan : OsmFilterQuestType<DietAvailabilityAnswer>() {
 
     override fun createForm() = AddDietTypeForm.create(R.string.quest_dietType_explanation_vegan)
 
-    override fun applyAnswerTo(answer: DietAvailabilityAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: DietAvailabilityAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is DietAvailability -> tags.updateWithCheckDate("diet:vegan", answer.osmValue)
             NoFood -> tags["food"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddVegetarian.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddVegetarian.kt
index 5e8778f067c..ad33854a10e 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddVegetarian.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/diet_type/AddVegetarian.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.diet_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -38,7 +39,7 @@ class AddVegetarian : OsmFilterQuestType<DietAvailabilityAnswer>() {
 
     override fun createForm() = AddDietTypeForm.create(R.string.quest_dietType_explanation_vegetarian)
 
-    override fun applyAnswerTo(answer: DietAvailabilityAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: DietAvailabilityAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is DietAvailability -> {
                 tags.updateWithCheckDate("diet:vegetarian", answer.osmValue)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/drinking_water/AddDrinkingWater.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/drinking_water/AddDrinkingWater.kt
index b36f4e7de84..2e6eeb24fb5 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/drinking_water/AddDrinkingWater.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/drinking_water/AddDrinkingWater.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.drinking_water
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -39,7 +40,7 @@ class AddDrinkingWater : OsmFilterQuestType<DrinkingWater>() {
 
     override fun createForm() = AddDrinkingWaterForm()
 
-    override fun applyAnswerTo(answer: DrinkingWater, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: DrinkingWater, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["drinking_water"] = answer.osmValue
         answer.osmLegalValue?.let { tags["drinking_water:legal"] = it }
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/drinking_water_type/AddDrinkingWaterType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/drinking_water_type/AddDrinkingWaterType.kt
index 7acdc0bef86..76a00dfc470 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/drinking_water_type/AddDrinkingWaterType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/drinking_water_type/AddDrinkingWaterType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.drinking_water_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.OUTDOORS
@@ -29,7 +30,7 @@ class AddDrinkingWaterType : OsmFilterQuestType<DrinkingWaterType>() {
 
     override fun createForm() = AddDrinkingWaterTypeForm()
 
-    override fun applyAnswerTo(answer: DrinkingWaterType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: DrinkingWaterType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         answer.applyTo(tags)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/existence/CheckExistence.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/existence/CheckExistence.kt
index bce5b25af1e..f3f7714dc24 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/existence/CheckExistence.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/existence/CheckExistence.kt
@@ -3,6 +3,7 @@ package de.westnordost.streetcomplete.quests.existence
 import de.westnordost.osmfeatures.FeatureDictionary
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -114,7 +115,7 @@ class CheckExistence(
 
     override fun createForm() = CheckExistenceForm()
 
-    override fun applyAnswerTo(answer: Unit, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Unit, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateCheckDate()
     }
 
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/ferry/AddFerryAccessMotorVehicle.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/ferry/AddFerryAccessMotorVehicle.kt
index 17396936209..8b8eaa5d676 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/ferry/AddFerryAccessMotorVehicle.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/ferry/AddFerryAccessMotorVehicle.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.ferry
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.RARE
@@ -21,7 +22,7 @@ class AddFerryAccessMotorVehicle : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["motor_vehicle"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/ferry/AddFerryAccessPedestrian.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/ferry/AddFerryAccessPedestrian.kt
index a99d7675c19..69c10f6f93b 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/ferry/AddFerryAccessPedestrian.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/ferry/AddFerryAccessPedestrian.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.ferry
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.RARE
@@ -21,7 +22,7 @@ class AddFerryAccessPedestrian : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["foot"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant/AddFireHydrantType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant/AddFireHydrantType.kt
index f7658d54db3..d3076616b26 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant/AddFireHydrantType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant/AddFireHydrantType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.fire_hydrant
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -24,7 +25,7 @@ class AddFireHydrantType : OsmFilterQuestType<FireHydrantType>() {
 
     override fun createForm() = AddFireHydrantTypeForm()
 
-    override fun applyAnswerTo(answer: FireHydrantType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: FireHydrantType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["fire_hydrant:type"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_diameter/AddFireHydrantDiameter.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_diameter/AddFireHydrantDiameter.kt
index c8942f37033..cc142711327 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_diameter/AddFireHydrantDiameter.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_diameter/AddFireHydrantDiameter.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.fire_hydrant_diameter
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -36,7 +37,7 @@ class AddFireHydrantDiameter : OsmFilterQuestType<FireHydrantDiameterAnswer>() {
 
     override fun createForm() = AddFireHydrantDiameterForm()
 
-    override fun applyAnswerTo(answer: FireHydrantDiameterAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: FireHydrantDiameterAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is FireHydrantDiameter ->       tags["fire_hydrant:diameter"] = answer.toOsmValue()
             is NoFireHydrantDiameterSign -> tags["fire_hydrant:diameter:signed"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_position/AddFireHydrantPosition.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_position/AddFireHydrantPosition.kt
index 6a1d429c43b..4703ec680a9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_position/AddFireHydrantPosition.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_position/AddFireHydrantPosition.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.fire_hydrant_position
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -29,7 +30,7 @@ class AddFireHydrantPosition : OsmFilterQuestType<FireHydrantPosition>() {
 
     override fun createForm() = AddFireHydrantPositionForm()
 
-    override fun applyAnswerTo(answer: FireHydrantPosition, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: FireHydrantPosition, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["fire_hydrant:position"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_ref/AddFireHydrantRef.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_ref/AddFireHydrantRef.kt
index 19167a9cd6c..8e6889d77fc 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_ref/AddFireHydrantRef.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/fire_hydrant_ref/AddFireHydrantRef.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.fire_hydrant_ref
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.quest.NoCountriesExcept
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement
@@ -26,7 +27,7 @@ class AddFireHydrantRef : OsmFilterQuestType<FireHydrantRefAnswer>() {
 
     override fun createForm() = AddFireHydrantRefForm()
 
-    override fun applyAnswerTo(answer: FireHydrantRefAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: FireHydrantRefAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is NoVisibleFireHydrantRef -> tags["ref:signed"] = "no"
             is FireHydrantRef ->          tags["ref"] = answer.ref
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/foot/AddProhibitedForPedestrians.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/foot/AddProhibitedForPedestrians.kt
index ea4b290086b..022cd7c0466 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/foot/AddProhibitedForPedestrians.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/foot/AddProhibitedForPedestrians.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.foot
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.osm.ANYTHING_PAVED
@@ -50,7 +51,7 @@ class AddProhibitedForPedestrians : OsmFilterQuestType<ProhibitedForPedestriansA
 
     override fun createForm() = AddProhibitedForPedestriansForm()
 
-    override fun applyAnswerTo(answer: ProhibitedForPedestriansAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: ProhibitedForPedestriansAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             // the question is whether it is prohibited, so YES -> foot=no etc
             YES -> tags["foot"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/fuel_service/AddFuelSelfService.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/fuel_service/AddFuelSelfService.kt
index 973a62c6d0d..8dbea64eaa7 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/fuel_service/AddFuelSelfService.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/fuel_service/AddFuelSelfService.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.fuel_service
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.quest.NoCountriesExcept
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
@@ -26,7 +27,7 @@ class AddFuelSelfService : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["self_service"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/general_fee/AddGeneralFee.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/general_fee/AddGeneralFee.kt
index 2656c7709ac..f1234105ad7 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/general_fee/AddGeneralFee.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/general_fee/AddGeneralFee.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.general_fee
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
 import de.westnordost.streetcomplete.osm.Tags
@@ -24,7 +25,7 @@ class AddGeneralFee : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["fee"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/handrail/AddHandrail.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/handrail/AddHandrail.kt
index c698a992722..4d2e0b574c3 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/handrail/AddHandrail.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/handrail/AddHandrail.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.handrail
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.WHEELCHAIR
@@ -33,7 +34,7 @@ class AddHandrail : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("handrail", answer.toYesNo())
         if (!answer) {
             tags.remove("handrail:left")
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/incline_direction/AddBicycleIncline.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/incline_direction/AddBicycleIncline.kt
index 3cd283b6772..6dadbe720f7 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/incline_direction/AddBicycleIncline.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/incline_direction/AddBicycleIncline.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.incline_direction
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
@@ -49,6 +50,6 @@ class AddBicycleIncline : OsmElementQuestType<BicycleInclineAnswer> {
 
     override fun createForm() = AddBicycleInclineForm()
 
-    override fun applyAnswerTo(answer: BicycleInclineAnswer, tags: Tags, timestampEdited: Long) =
+    override fun applyAnswerTo(answer: BicycleInclineAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) =
         answer.applyTo(tags)
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/incline_direction/AddStepsIncline.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/incline_direction/AddStepsIncline.kt
index 65edcc701ab..cee576352e0 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/incline_direction/AddStepsIncline.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/incline_direction/AddStepsIncline.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.incline_direction
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.osm.Tags
@@ -23,6 +24,6 @@ class AddStepsIncline : OsmFilterQuestType<Incline>() {
 
     override fun createForm() = AddInclineForm()
 
-    override fun applyAnswerTo(answer: Incline, tags: Tags, timestampEdited: Long) =
+    override fun applyAnswerTo(answer: Incline, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) =
         answer.applyTo(tags)
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/internet_access/AddInternetAccess.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/internet_access/AddInternetAccess.kt
index 48bc536d937..d4d16fdbfdf 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/internet_access/AddInternetAccess.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/internet_access/AddInternetAccess.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.internet_access
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
 import de.westnordost.streetcomplete.osm.Tags
@@ -35,7 +36,7 @@ class AddInternetAccess : OsmFilterQuestType<InternetAccess>() {
 
     override fun createForm() = AddInternetAccessForm()
 
-    override fun applyAnswerTo(answer: InternetAccess, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: InternetAccess, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("internet_access", answer.osmValue)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/kerb_height/AddKerbHeight.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/kerb_height/AddKerbHeight.kt
index 24f4650ee50..e14b6ebbde9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/kerb_height/AddKerbHeight.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/kerb_height/AddKerbHeight.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.kerb_height
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Node
@@ -39,7 +40,7 @@ class AddKerbHeight : OsmElementQuestType<KerbHeight> {
 
     override fun createForm() = AddKerbHeightForm()
 
-    override fun applyAnswerTo(answer: KerbHeight, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: KerbHeight, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("kerb", answer.osmValue)
         if (answer.osmValue == "no") {
             tags.remove("barrier")
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/lanes/AddLanes.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/lanes/AddLanes.kt
index ffc225b6cdd..9be325a06cc 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/lanes/AddLanes.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/lanes/AddLanes.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.lanes
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
 import de.westnordost.streetcomplete.osm.ANYTHING_PAVED
@@ -32,7 +33,7 @@ class AddLanes : OsmFilterQuestType<LanesAnswer>() {
 
     override fun createForm() = AddLanesForm()
 
-    override fun applyAnswerTo(answer: LanesAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: LanesAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
 
         val laneCount = answer.total
 
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/leaf_detail/AddForestLeafType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/leaf_detail/AddForestLeafType.kt
index 708117e1e81..934ab1e6f37 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/leaf_detail/AddForestLeafType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/leaf_detail/AddForestLeafType.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.leaf_detail
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.geometry.ElementPolygonsGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
@@ -47,7 +48,7 @@ class AddForestLeafType : OsmElementQuestType<ForestLeafType> {
 
     override fun createForm() = AddForestLeafTypeForm()
 
-    override fun applyAnswerTo(answer: ForestLeafType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: ForestLeafType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["leaf_type"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/level/AddLevel.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/level/AddLevel.kt
index 04650a6e4a0..a9d3a77ff1c 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/level/AddLevel.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/level/AddLevel.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.level
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.geometry.ElementPolygonsGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
@@ -112,7 +113,7 @@ class AddLevel : OsmElementQuestType<String> {
 
     override fun createForm() = AddLevelForm()
 
-    override fun applyAnswerTo(answer: String, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: String, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["level"] = answer
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxHeight.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxHeight.kt
index 6ca923b2119..8a1c84c8b77 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxHeight.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxHeight.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.max_height
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
@@ -122,7 +123,7 @@ class AddMaxHeight : OsmElementQuestType<MaxHeightAnswer> {
 
     override fun createForm() = AddMaxHeightForm()
 
-    override fun applyAnswerTo(answer: MaxHeightAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: MaxHeightAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is MaxHeight -> {
                 tags["maxheight"] = answer.value.toOsmValue()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxPhysicalHeight.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxPhysicalHeight.kt
index 1df47b1c930..2684b7d0fc3 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxPhysicalHeight.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/max_height/AddMaxPhysicalHeight.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.max_height
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -62,7 +63,7 @@ class AddMaxPhysicalHeight(
 
     override fun createForm() = AddMaxPhysicalHeightForm()
 
-    override fun applyAnswerTo(answer: MaxPhysicalHeightAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: MaxPhysicalHeightAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         // overwrite maxheight value but retain the info that there is no sign onto another tag
         tags["maxheight"] = answer.height.toOsmValue()
         tags["maxheight:signed"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/max_speed/AddMaxSpeed.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/max_speed/AddMaxSpeed.kt
index d569dc2e706..b8bd4a6c728 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/max_speed/AddMaxSpeed.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/max_speed/AddMaxSpeed.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.max_speed
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.quest.AllCountriesExcept
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
@@ -36,7 +37,7 @@ class AddMaxSpeed : OsmFilterQuestType<MaxSpeedAnswer>() {
 
     override fun createForm() = AddMaxSpeedForm()
 
-    override fun applyAnswerTo(answer: MaxSpeedAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: MaxSpeedAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is MaxSpeedSign -> {
                 tags["maxspeed"] = answer.value.toString()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/max_weight/AddMaxWeight.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/max_weight/AddMaxWeight.kt
index 553a1634cce..02fcc541de0 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/max_weight/AddMaxWeight.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/max_weight/AddMaxWeight.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.max_weight
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
 import de.westnordost.streetcomplete.osm.Tags
@@ -37,7 +38,7 @@ class AddMaxWeight : OsmFilterQuestType<MaxWeightAnswer>() {
 
     override fun createForm() = AddMaxWeightForm()
 
-    override fun applyAnswerTo(answer: MaxWeightAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: MaxWeightAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is MaxWeight -> {
                 tags[answer.sign.osmKey] = answer.weight.toString()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/memorial_type/AddMemorialType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/memorial_type/AddMemorialType.kt
index a67ad290107..2d5f3c35eb3 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/memorial_type/AddMemorialType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/memorial_type/AddMemorialType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.memorial_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
 import de.westnordost.streetcomplete.osm.Tags
@@ -22,7 +23,7 @@ class AddMemorialType : OsmFilterQuestType<MemorialType>() {
 
     override fun createForm() = AddMemorialTypeForm()
 
-    override fun applyAnswerTo(answer: MemorialType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: MemorialType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         answer.applyTo(tags)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/motorcycle_parking_capacity/AddMotorcycleParkingCapacity.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/motorcycle_parking_capacity/AddMotorcycleParkingCapacity.kt
index 73fa4fe2cd0..f6f0653bd84 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/motorcycle_parking_capacity/AddMotorcycleParkingCapacity.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/motorcycle_parking_capacity/AddMotorcycleParkingCapacity.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.motorcycle_parking_capacity
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -29,7 +30,7 @@ class AddMotorcycleParkingCapacity : OsmFilterQuestType<Int>() {
     override fun getHighlightedElements(element: Element, getMapData: () -> MapDataWithGeometry) =
         getMapData().filter("nodes, ways with amenity = motorcycle_parking")
 
-    override fun applyAnswerTo(answer: Int, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Int, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("capacity", answer.toString())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/motorcycle_parking_cover/AddMotorcycleParkingCover.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/motorcycle_parking_cover/AddMotorcycleParkingCover.kt
index 74886df5623..23d36d24e51 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/motorcycle_parking_cover/AddMotorcycleParkingCover.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/motorcycle_parking_cover/AddMotorcycleParkingCover.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.motorcycle_parking_cover
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -31,7 +32,7 @@ class AddMotorcycleParkingCover : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["covered"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/oneway/AddOneway.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/oneway/AddOneway.kt
index 3c301cbd554..52ed5a2ef6a 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/oneway/AddOneway.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/oneway/AddOneway.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.oneway
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Way
@@ -79,7 +80,7 @@ class AddOneway : OsmElementQuestType<OnewayAnswer> {
 
     override fun createForm() = AddOnewayForm()
 
-    override fun applyAnswerTo(answer: OnewayAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: OnewayAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["oneway"] = when (answer) {
             FORWARD -> "yes"
             BACKWARD -> "-1"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOneway.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOneway.kt
index f983649993e..bd3e5f92d80 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOneway.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOneway.kt
@@ -3,6 +3,7 @@ package de.westnordost.streetcomplete.quests.oneway_suspects
 import android.util.Log
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
@@ -134,7 +135,7 @@ class AddSuspectedOneway(
 
     override fun createForm() = AddSuspectedOnewayForm()
 
-    override fun applyAnswerTo(answer: SuspectedOnewayAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SuspectedOnewayAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         if (!answer.isOneway) {
             tags["oneway"] = "no"
         } else {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHours.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHours.kt
index d7ee8e7df68..c88f7051558 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHours.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHours.kt
@@ -5,6 +5,7 @@ import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.filters.RelativeDate
 import de.westnordost.streetcomplete.data.elementfilter.filters.TagOlderThan
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -156,7 +157,7 @@ class AddOpeningHours(
 
     override fun createForm() = AddOpeningHoursForm()
 
-    override fun applyAnswerTo(answer: OpeningHoursAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: OpeningHoursAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         if (answer is NoOpeningHoursSign) {
             tags["opening_hours:signed"] = "no"
             tags.updateCheckDateForKey("opening_hours")
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt
index e8d02c4fc18..a88b57de1e0 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt
@@ -3,6 +3,7 @@ package de.westnordost.streetcomplete.quests.opening_hours_signed
 import de.westnordost.osmfeatures.FeatureDictionary
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -62,7 +63,7 @@ class CheckOpeningHoursSigned(
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
 
         if (answer) {
             tags.remove("opening_hours:signed")
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/orchard_produce/AddOrchardProduce.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/orchard_produce/AddOrchardProduce.kt
index 2dea47d987e..ec33a2825f3 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/orchard_produce/AddOrchardProduce.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/orchard_produce/AddOrchardProduce.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.orchard_produce
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.OUTDOORS
 import de.westnordost.streetcomplete.osm.Tags
@@ -22,7 +23,7 @@ class AddOrchardProduce : OsmFilterQuestType<List<OrchardProduce>>() {
 
     override fun createForm() = AddOrchardProduceForm()
 
-    override fun applyAnswerTo(answer: List<OrchardProduce>, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: List<OrchardProduce>, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["produce"] = answer.joinToString(";") { it.osmValue }
 
         val landuse = answer.singleOrNull()?.osmLanduseValue
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_access/AddBikeParkingAccess.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_access/AddBikeParkingAccess.kt
index b49b8cb3e82..9ecd16e4968 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_access/AddBikeParkingAccess.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_access/AddBikeParkingAccess.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.parking_access
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.osm.Tags
@@ -24,7 +25,7 @@ class AddBikeParkingAccess : OsmFilterQuestType<ParkingAccess>() {
 
     override fun createForm() = AddParkingAccessForm()
 
-    override fun applyAnswerTo(answer: ParkingAccess, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: ParkingAccess, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["access"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_access/AddParkingAccess.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_access/AddParkingAccess.kt
index 3573521716f..0c811a62f7f 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_access/AddParkingAccess.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_access/AddParkingAccess.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.parking_access
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
 import de.westnordost.streetcomplete.osm.Tags
@@ -38,7 +39,7 @@ class AddParkingAccess : OsmFilterQuestType<ParkingAccess>() {
 
     override fun createForm() = AddParkingAccessForm()
 
-    override fun applyAnswerTo(answer: ParkingAccess, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: ParkingAccess, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["access"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddBikeParkingFee.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddBikeParkingFee.kt
index f0e59d46213..1b782dad339 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddBikeParkingFee.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddBikeParkingFee.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.parking_fee
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.osm.Tags
@@ -30,6 +31,6 @@ class AddBikeParkingFee : OsmFilterQuestType<FeeAndMaxStay>() {
 
     override fun createForm() = AddParkingFeeForm()
 
-    override fun applyAnswerTo(answer: FeeAndMaxStay, tags: Tags, timestampEdited: Long) =
+    override fun applyAnswerTo(answer: FeeAndMaxStay, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) =
         answer.applyTo(tags)
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFee.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFee.kt
index cf912383da6..654e8cd6474 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFee.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFee.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.parking_fee
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
 import de.westnordost.streetcomplete.osm.Tags
@@ -24,6 +25,6 @@ class AddParkingFee : OsmFilterQuestType<FeeAndMaxStay>() {
 
     override fun createForm() = AddParkingFeeForm()
 
-    override fun applyAnswerTo(answer: FeeAndMaxStay, tags: Tags, timestampEdited: Long) =
+    override fun applyAnswerTo(answer: FeeAndMaxStay, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) =
         answer.applyTo(tags)
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_type/AddParkingType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_type/AddParkingType.kt
index 3ffb687bd91..eed0f5185f9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/parking_type/AddParkingType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/parking_type/AddParkingType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.parking_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
 import de.westnordost.streetcomplete.osm.Tags
@@ -21,7 +22,7 @@ class AddParkingType : OsmFilterQuestType<ParkingType>() {
 
     override fun createForm() = AddParkingTypeForm()
 
-    override fun applyAnswerTo(answer: ParkingType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: ParkingType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["parking"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/picnic_table_cover/AddPicnicTableCover.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/picnic_table_cover/AddPicnicTableCover.kt
index b93c4e73514..309adb9a35f 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/picnic_table_cover/AddPicnicTableCover.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/picnic_table_cover/AddPicnicTableCover.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.picnic_table_cover
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -32,7 +33,7 @@ class AddPicnicTableCover : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["covered"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/pitch_lit/AddPitchLit.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/pitch_lit/AddPitchLit.kt
index a0176b8a7dc..b9be1b944a0 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/pitch_lit/AddPitchLit.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/pitch_lit/AddPitchLit.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.pitch_lit
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.OUTDOORS
 import de.westnordost.streetcomplete.osm.Tags
@@ -29,7 +30,7 @@ class AddPitchLit : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("lit", answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/place_name/AddPlaceName.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/place_name/AddPlaceName.kt
index a3fb6a8f695..2b98fc49261 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/place_name/AddPlaceName.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/place_name/AddPlaceName.kt
@@ -3,6 +3,7 @@ package de.westnordost.streetcomplete.quests.place_name
 import de.westnordost.osmfeatures.FeatureDictionary
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -128,7 +129,7 @@ class AddPlaceName(
 
     override fun createForm() = AddPlaceNameForm()
 
-    override fun applyAnswerTo(answer: PlaceNameAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: PlaceNameAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is NoPlaceNameSign -> {
                 tags["name:signed"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/playground_access/AddPlaygroundAccess.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/playground_access/AddPlaygroundAccess.kt
index 0bbd165a7a4..9d3f8e8b631 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/playground_access/AddPlaygroundAccess.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/playground_access/AddPlaygroundAccess.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.playground_access
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
 import de.westnordost.streetcomplete.osm.Tags
@@ -17,7 +18,7 @@ class AddPlaygroundAccess : OsmFilterQuestType<PlaygroundAccess>() {
 
     override fun createForm() = AddPlaygroundAccessForm()
 
-    override fun applyAnswerTo(answer: PlaygroundAccess, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: PlaygroundAccess, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["access"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/police_type/AddPoliceType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/police_type/AddPoliceType.kt
index c03c9594965..475e67530b3 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/police_type/AddPoliceType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/police_type/AddPoliceType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.police_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.quest.NoCountriesExcept
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
@@ -19,7 +20,7 @@ class AddPoliceType : OsmFilterQuestType<PoliceType>() {
 
     override fun createForm() = AddPoliceTypeForm()
 
-    override fun applyAnswerTo(answer: PoliceType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: PoliceType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["operator"] = answer.operatorName
         tags["operator:wikidata"] = answer.wikidata
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_collection_times/AddPostboxCollectionTimes.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_collection_times/AddPostboxCollectionTimes.kt
index 89703e722bf..d952d8b989d 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_collection_times/AddPostboxCollectionTimes.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_collection_times/AddPostboxCollectionTimes.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.postbox_collection_times
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -79,7 +80,7 @@ class AddPostboxCollectionTimes : OsmElementQuestType<CollectionTimesAnswer> {
 
     override fun createForm() = AddPostboxCollectionTimesForm()
 
-    override fun applyAnswerTo(answer: CollectionTimesAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: CollectionTimesAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is NoCollectionTimesSign -> {
                 tags["collection_times:signed"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_ref/AddPostboxRef.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_ref/AddPostboxRef.kt
index aca387761f7..a490bd91d14 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_ref/AddPostboxRef.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_ref/AddPostboxRef.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.postbox_ref
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -33,7 +34,7 @@ class AddPostboxRef : OsmFilterQuestType<PostboxRefAnswer>() {
 
     override fun createForm() = AddPostboxRefForm()
 
-    override fun applyAnswerTo(answer: PostboxRefAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: PostboxRefAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is NoVisiblePostboxRef -> tags["ref:signed"] = "no"
             is PostboxRef ->          tags["ref"] = answer.ref
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_royal_cypher/AddPostboxRoyalCypher.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_royal_cypher/AddPostboxRoyalCypher.kt
index d9b829b81c3..5c8352533b7 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_royal_cypher/AddPostboxRoyalCypher.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/postbox_royal_cypher/AddPostboxRoyalCypher.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.postbox_royal_cypher
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -31,7 +32,7 @@ class AddPostboxRoyalCypher : OsmFilterQuestType<PostboxRoyalCypher>() {
 
     override fun createForm() = AddPostboxRoyalCypherForm()
 
-    override fun applyAnswerTo(answer: PostboxRoyalCypher, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: PostboxRoyalCypher, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["royal_cypher"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/powerpoles_material/AddPowerPolesMaterial.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/powerpoles_material/AddPowerPolesMaterial.kt
index 10da20e391a..cc95a3e278f 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/powerpoles_material/AddPowerPolesMaterial.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/powerpoles_material/AddPowerPolesMaterial.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.powerpoles_material
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -35,7 +36,7 @@ class AddPowerPolesMaterial : OsmFilterQuestType<PowerPolesMaterial>() {
 
     override fun createForm() = AddPowerPolesMaterialForm()
 
-    override fun applyAnswerTo(answer: PowerPolesMaterial, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: PowerPolesMaterial, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["material"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/railway_crossing/AddRailwayCrossingBarrier.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/railway_crossing/AddRailwayCrossingBarrier.kt
index 3d6884f466f..e1fe9467dc9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/railway_crossing/AddRailwayCrossingBarrier.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/railway_crossing/AddRailwayCrossingBarrier.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.railway_crossing
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -51,7 +52,7 @@ class AddRailwayCrossingBarrier : OsmElementQuestType<RailwayCrossingBarrier> {
 
     override fun createForm() = AddRailwayCrossingBarrierForm()
 
-    override fun applyAnswerTo(answer: RailwayCrossingBarrier, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: RailwayCrossingBarrier, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         if (answer.osmValue != null) {
             tags.remove("crossing:chicane")
             tags.updateWithCheckDate("crossing:barrier", answer.osmValue)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/recycling/AddRecyclingType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/recycling/AddRecyclingType.kt
index 032f794a4e1..b4ddaeb04a0 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/recycling/AddRecyclingType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/recycling/AddRecyclingType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.recycling
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -27,7 +28,7 @@ class AddRecyclingType : OsmFilterQuestType<RecyclingType>() {
 
     override fun createForm() = AddRecyclingTypeForm()
 
-    override fun applyAnswerTo(answer: RecyclingType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: RecyclingType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             RECYCLING_CENTRE -> {
                 tags["recycling_type"] = "centre"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/recycling_glass/DetermineRecyclingGlass.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/recycling_glass/DetermineRecyclingGlass.kt
index 798d0167b6e..4bb95f39bd6 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/recycling_glass/DetermineRecyclingGlass.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/recycling_glass/DetermineRecyclingGlass.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.recycling_glass
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -35,7 +36,7 @@ class DetermineRecyclingGlass : OsmFilterQuestType<RecyclingGlass>() {
 
     override fun createForm() = DetermineRecyclingGlassForm()
 
-    override fun applyAnswerTo(answer: RecyclingGlass, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: RecyclingGlass, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             ANY -> {
                 // to mark that it has been checked
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/recycling_material/AddRecyclingContainerMaterials.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/recycling_material/AddRecyclingContainerMaterials.kt
index f87bf6bfb57..58a56ea8066 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/recycling_material/AddRecyclingContainerMaterials.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/recycling_material/AddRecyclingContainerMaterials.kt
@@ -4,6 +4,7 @@ import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.filters.RelativeDate
 import de.westnordost.streetcomplete.data.elementfilter.filters.TagOlderThan
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -52,7 +53,7 @@ class AddRecyclingContainerMaterials : OsmElementQuestType<RecyclingContainerMat
     override fun getHighlightedElements(element: Element, getMapData: () -> MapDataWithGeometry) =
         getMapData().filter("nodes with amenity = recycling")
 
-    override fun applyAnswerTo(answer: RecyclingContainerMaterialsAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: RecyclingContainerMaterialsAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         if (answer is RecyclingMaterials) {
             applyRecyclingMaterialsAnswer(answer.materials, tags)
         } else if (answer is IsWasteContainer) {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/religion/AddReligionToPlaceOfWorship.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/religion/AddReligionToPlaceOfWorship.kt
index 28a2835bbf9..68858a95ab9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/religion/AddReligionToPlaceOfWorship.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/religion/AddReligionToPlaceOfWorship.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.religion
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
 import de.westnordost.streetcomplete.osm.Tags
@@ -25,7 +26,7 @@ class AddReligionToPlaceOfWorship : OsmFilterQuestType<Religion>() {
 
     override fun createForm() = AddReligionForm()
 
-    override fun applyAnswerTo(answer: Religion, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Religion, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["religion"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/religion/AddReligionToWaysideShrine.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/religion/AddReligionToWaysideShrine.kt
index 4b08e33943b..7509717c411 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/religion/AddReligionToWaysideShrine.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/religion/AddReligionToWaysideShrine.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.religion
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.OUTDOORS
 import de.westnordost.streetcomplete.osm.Tags
@@ -22,7 +23,7 @@ class AddReligionToWaysideShrine : OsmFilterQuestType<Religion>() {
 
     override fun createForm() = AddReligionForm()
 
-    override fun applyAnswerTo(answer: Religion, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Religion, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["religion"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/road_name/AddRoadName.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/road_name/AddRoadName.kt
index 5ddedaf45e8..ea8ea5d63d8 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/road_name/AddRoadName.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/road_name/AddRoadName.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.road_name
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.quest.AllCountriesExcept
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
@@ -36,7 +37,7 @@ class AddRoadName : OsmFilterQuestType<RoadNameAnswer>() {
 
     override fun createForm() = AddRoadNameForm()
 
-    override fun applyAnswerTo(answer: RoadNameAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: RoadNameAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is NoRoadName -> tags["noname"] = "yes"
             is RoadIsServiceRoad -> {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/roof_shape/AddRoofShape.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/roof_shape/AddRoofShape.kt
index 8df4669efbd..89b7a282031 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/roof_shape/AddRoofShape.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/roof_shape/AddRoofShape.kt
@@ -5,6 +5,7 @@ import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
 import de.westnordost.streetcomplete.data.meta.CountryInfos
 import de.westnordost.streetcomplete.data.meta.getByLocation
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -64,7 +65,7 @@ class AddRoofShape(
         ).roofsAreUsuallyFlat
     }
 
-    override fun applyAnswerTo(answer: RoofShape, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: RoofShape, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["roof:shape"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/seating/AddSeating.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/seating/AddSeating.kt
index a24713456ad..8126bdfdfad 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/seating/AddSeating.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/seating/AddSeating.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.seating
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -35,7 +36,7 @@ class AddSeating : OsmFilterQuestType<Seating>() {
 
     override fun createForm() = AddSeatingForm()
 
-    override fun applyAnswerTo(answer: Seating, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Seating, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         if (answer == Seating.NO) tags["takeaway"] = "only"
         tags["outdoor_seating"] = answer.hasOutdoorSeating.toYesNo()
         tags["indoor_seating"] = answer.hasIndoorSeating.toYesNo()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/segregated/AddCyclewaySegregation.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/segregated/AddCyclewaySegregation.kt
index f4d0139d4df..9b288bc2d90 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/segregated/AddCyclewaySegregation.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/segregated/AddCyclewaySegregation.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.segregated
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.OUTDOORS
@@ -32,7 +33,7 @@ class AddCyclewaySegregation : OsmFilterQuestType<CyclewaySegregation>() {
 
     override fun createForm() = AddCyclewaySegregationForm()
 
-    override fun applyAnswerTo(answer: CyclewaySegregation, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: CyclewaySegregation, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("segregated", answer.value.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/segregated/CyclewaySegregationItem.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/segregated/CyclewaySegregationItem.kt
index c0b483a06b7..216574dfd88 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/segregated/CyclewaySegregationItem.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/segregated/CyclewaySegregationItem.kt
@@ -12,6 +12,6 @@ private val CyclewaySegregation.titleResId: Int get() = when (this.value) {
 }
 
 private fun CyclewaySegregation.getIconResId(isLeftHandTraffic: Boolean): Int = when (this.value) {
-    true -> if (isLeftHandTraffic) R.drawable.ic_path_segregated_l else R.drawable.ic_path_segregated
-    false -> R.drawable.ic_path_segregated_no
+    true -> if (isLeftHandTraffic) R.drawable.ic_separate_cycleway_segregated_l else R.drawable.ic_separate_cycleway_segregated
+    false -> R.drawable.ic_separate_cycleway_not_segregated
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/self_service/AddSelfServiceLaundry.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/self_service/AddSelfServiceLaundry.kt
index 7961f293eff..e5c1ab097c9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/self_service/AddSelfServiceLaundry.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/self_service/AddSelfServiceLaundry.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.self_service
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
 import de.westnordost.streetcomplete.osm.Tags
@@ -21,7 +22,7 @@ class AddSelfServiceLaundry : OsmFilterQuestType<SelfServiceLaundry>() {
 
     override fun createForm() = AddSelfServiceLaundryForm()
 
-    override fun applyAnswerTo(answer: SelfServiceLaundry, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SelfServiceLaundry, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             NO -> {
                 tags["self_service"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopType.kt
index 9eb498386d6..f30d7c3f682 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopType.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.shop_type
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -49,7 +50,7 @@ class CheckShopType : OsmElementQuestType<ShopTypeAnswer> {
 
     override fun createForm() = ShopTypeForm()
 
-    override fun applyAnswerTo(answer: ShopTypeAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: ShopTypeAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is IsShopVacant -> {
                 tags.updateCheckDate()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/SpecifyShopType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/SpecifyShopType.kt
index 016327c9ec2..dd22d0f90e4 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/SpecifyShopType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/SpecifyShopType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.shop_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -42,7 +43,7 @@ class SpecifyShopType : OsmFilterQuestType<ShopTypeAnswer>() {
 
     override fun createForm() = ShopTypeForm()
 
-    override fun applyAnswerTo(answer: ShopTypeAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: ShopTypeAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.removeCheckDates()
         when (answer) {
             is IsShopVacant -> {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/shoulder/AddShoulder.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/shoulder/AddShoulder.kt
index 7759c80eb5a..1bf922279a7 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/shoulder/AddShoulder.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/shoulder/AddShoulder.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.shoulder
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
 import de.westnordost.streetcomplete.osm.ANYTHING_UNPAVED
@@ -60,7 +61,7 @@ class AddShoulder : OsmFilterQuestType<ShoulderSides>() {
 
     override fun createForm() = AddShoulderForm()
 
-    override fun applyAnswerTo(answer: ShoulderSides, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: ShoulderSides, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["shoulder"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt
index f6f42dca73e..c28c6cc77c1 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.sidewalk
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
@@ -15,8 +16,8 @@ import de.westnordost.streetcomplete.osm.estimateParkingOffRoadWidth
 import de.westnordost.streetcomplete.osm.estimateRoadwayWidth
 import de.westnordost.streetcomplete.osm.guessRoadwayWidth
 import de.westnordost.streetcomplete.osm.sidewalk.LeftAndRightSidewalk
-import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk
 import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.INVALID
+import de.westnordost.streetcomplete.osm.sidewalk.any
 import de.westnordost.streetcomplete.osm.sidewalk.applyTo
 import de.westnordost.streetcomplete.osm.sidewalk.createSidewalkSides
 import de.westnordost.streetcomplete.util.math.isNearAndAligned
@@ -100,69 +101,66 @@ class AddSidewalk : OsmElementQuestType<LeftAndRightSidewalk> {
 
     override fun createForm() = AddSidewalkForm()
 
-    override fun applyAnswerTo(answer: LeftAndRightSidewalk, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: LeftAndRightSidewalk, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         answer.applyTo(tags)
     }
-
-    companion object {
-        // streets that may have sidewalk tagging
-        private val roadsFilter by lazy { """
-            ways with
-              (
-                (
-                  highway ~ trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified|residential|service
-                  and motorroad != yes
-                  and foot != no
-                )
-                or
-                (
-                  highway ~ motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified|residential|service
-                  and (foot ~ yes|designated or bicycle ~ yes|designated)
-                )
-              )
-              and area != yes
-              and access !~ private|no
-        """.toElementFilterExpression() }
-
-        // streets that do not have sidewalk tagging yet
-        /* the filter additionally filters out ways that are unlikely to have sidewalks:
-         *
-         * + unpaved roads
-         * + roads that are probably not developed enough to have sidewalk (i.e. country roads)
-         * + roads with a very low speed limit
-         * + Also, anything explicitly tagged as no pedestrians or explicitly tagged that the sidewalk
-         *   is mapped as a separate way OR that is tagged with that the cycleway is separate. If the
-         *   cycleway is separate, the sidewalk is too for sure
-        * */
-        private val untaggedRoadsFilter by lazy { """
-            ways with
-              highway ~ motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified|residential
-              and !sidewalk and !sidewalk:both and !sidewalk:left and !sidewalk:right
-              and (!maxspeed or maxspeed > 9)
-              and surface !~ ${ANYTHING_UNPAVED.joinToString("|")}
-              and (
-                lit = yes
-                or highway = residential
-                or ~${(MAXSPEED_TYPE_KEYS + "maxspeed").joinToString("|")} ~ .*urban|.*zone.*
-                or (foot ~ yes|designated and highway ~ motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link)
-              )
-              and foot != use_sidepath
-              and bicycle != use_sidepath
-              and bicycle:backward != use_sidepath
-              and bicycle:forward != use_sidepath
-              and cycleway != separate
-              and cycleway:left != separate
-              and cycleway:right != separate
-              and cycleway:both != separate
-        """.toElementFilterExpression() }
-    }
-
-    private fun Element.hasInvalidOrIncompleteSidewalkTags(): Boolean {
-        val sides = createSidewalkSides(tags) ?: return false
-        if (sides.any { it == INVALID || it == null }) return true
-        return false
-    }
 }
 
-private fun LeftAndRightSidewalk.any(block: (sidewalk: Sidewalk?) -> Boolean): Boolean =
-    block(left) || block(right)
+// streets that may have sidewalk tagging
+private val roadsFilter by lazy { """
+    ways with
+      (
+        (
+          highway ~ trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified|residential|service
+          and motorroad != yes
+          and expressway != yes
+          and foot != no
+        )
+        or
+        (
+          highway ~ motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified|residential|service
+          and (foot ~ yes|designated or bicycle ~ yes|designated)
+        )
+      )
+      and area != yes
+      and access !~ private|no
+""".toElementFilterExpression() }
+
+// streets that do not have sidewalk tagging yet
+/* the filter additionally filters out ways that are unlikely to have sidewalks:
+ *
+ * + unpaved roads
+ * + roads that are probably not developed enough to have sidewalk (i.e. country roads)
+ * + roads with a very low speed limit
+ * + Also, anything explicitly tagged as no pedestrians or explicitly tagged that the sidewalk
+ *   is mapped as a separate way OR that is tagged with that the cycleway is separate. If the
+ *   cycleway is separate, the sidewalk is too for sure
+* */
+private val untaggedRoadsFilter by lazy { """
+    ways with
+      highway ~ motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified|residential
+      and !sidewalk and !sidewalk:both and !sidewalk:left and !sidewalk:right
+      and (!maxspeed or maxspeed > 9)
+      and surface !~ ${ANYTHING_UNPAVED.joinToString("|")}
+      and (
+        lit = yes
+        or highway = residential
+        or ~${(MAXSPEED_TYPE_KEYS + "maxspeed").joinToString("|")} ~ .*urban|.*zone.*
+        or (foot ~ yes|designated and highway ~ motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link)
+      )
+      and foot != use_sidepath
+      and bicycle != use_sidepath
+      and bicycle:backward != use_sidepath
+      and bicycle:forward != use_sidepath
+      and cycleway != separate
+      and cycleway:left != separate
+      and cycleway:right != separate
+      and cycleway:both != separate
+""".toElementFilterExpression() }
+
+
+private fun Element.hasInvalidOrIncompleteSidewalkTags(): Boolean {
+    val sides = createSidewalkSides(tags) ?: return false
+    if (sides.any { it == INVALID || it == null }) return true
+    return false
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/smoking/AddSmoking.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/smoking/AddSmoking.kt
index 3cfc295ce70..f515b520627 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/smoking/AddSmoking.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/smoking/AddSmoking.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.smoking
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -52,7 +53,7 @@ class AddSmoking : OsmFilterQuestType<SmokingAllowed>() {
 
     override fun createForm() = SmokingAllowedForm()
 
-    override fun applyAnswerTo(answer: SmokingAllowed, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SmokingAllowed, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("smoking", answer.osmValue)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/smoothness/AddPathSmoothness.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/smoothness/AddPathSmoothness.kt
index 60be94f3eaa..15b3bea6901 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/smoothness/AddPathSmoothness.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/smoothness/AddPathSmoothness.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.smoothness
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.WHEELCHAIR
@@ -33,7 +34,7 @@ class AddPathSmoothness : OsmFilterQuestType<SmoothnessAnswer>() {
 
     override fun createForm() = AddSmoothnessForm()
 
-    override fun applyAnswerTo(answer: SmoothnessAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SmoothnessAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         answer.applyTo(tags)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/smoothness/AddRoadSmoothness.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/smoothness/AddRoadSmoothness.kt
index 3fd830bae13..da1ab5f1aeb 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/smoothness/AddRoadSmoothness.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/smoothness/AddRoadSmoothness.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.smoothness
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
@@ -34,7 +35,7 @@ class AddRoadSmoothness : OsmFilterQuestType<SmoothnessAnswer>() {
 
     override fun createForm() = AddSmoothnessForm()
 
-    override fun applyAnswerTo(answer: SmoothnessAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SmoothnessAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         if (answer is IsActuallyStepsAnswer) throw IllegalStateException()
         answer.applyTo(tags)
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/sport/AddSport.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/sport/AddSport.kt
index 4267c8e2dca..832c4f85450 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/sport/AddSport.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/sport/AddSport.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.sport
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.OUTDOORS
 import de.westnordost.streetcomplete.osm.Tags
@@ -23,7 +24,7 @@ class AddSport : OsmFilterQuestType<List<Sport>>() {
 
     override fun createForm() = AddSportForm()
 
-    override fun applyAnswerTo(answer: List<Sport>, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: List<Sport>, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["sport"] = answer.joinToString(";") { it.osmValue }
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/step_count/AddStepCount.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/step_count/AddStepCount.kt
index 796085e9c55..3572c0855b8 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/step_count/AddStepCount.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/step_count/AddStepCount.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.step_count
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.osm.Tags
@@ -25,7 +26,7 @@ class AddStepCount : OsmFilterQuestType<Int>() {
 
     override fun createForm() = AddStepCountForm()
 
-    override fun applyAnswerTo(answer: Int, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Int, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["step_count"] = answer.toString()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/step_count/AddStepCountStile.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/step_count/AddStepCountStile.kt
index 0d31f5acd60..a301a9458be 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/step_count/AddStepCountStile.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/step_count/AddStepCountStile.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.step_count
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -46,7 +47,7 @@ class AddStepCountStile : OsmElementQuestType<Int> {
 
     override fun createForm() = AddStepCountForm.create(R.string.quest_step_count_stile_hint)
 
-    override fun applyAnswerTo(answer: Int, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Int, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["step_count"] = answer.toString()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/steps_ramp/AddStepsRamp.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/steps_ramp/AddStepsRamp.kt
index 4f97c2ea6ba..026cf2d6860 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/steps_ramp/AddStepsRamp.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/steps_ramp/AddStepsRamp.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.steps_ramp
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
@@ -33,7 +34,7 @@ class AddStepsRamp : OsmFilterQuestType<StepsRampAnswer>() {
 
     override fun createForm() = AddStepsRampForm()
 
-    override fun applyAnswerTo(answer: StepsRampAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: StepsRampAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         // special tagging if the wheelchair ramp is separate
         if (answer.wheelchairRamp == WheelchairRampStatus.SEPARATE) {
             val hasAnotherRamp = answer.bicycleRamp || answer.strollerRamp
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/summit/AddSummitCross.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/summit/AddSummitCross.kt
index 5acf55c2076..6825d1f4847 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/summit/AddSummitCross.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/summit/AddSummitCross.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.summit
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -72,7 +73,7 @@ class AddSummitCross : OsmElementQuestType<Boolean> {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("summit:cross", answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/summit/AddSummitRegister.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/summit/AddSummitRegister.kt
index c2c092ea314..30229431669 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/summit/AddSummitRegister.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/summit/AddSummitRegister.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.summit
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -79,7 +80,7 @@ class AddSummitRegister : OsmElementQuestType<Boolean> {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("summit:register", answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddCyclewayPartSurface.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddCyclewayPartSurface.kt
index 4e31900c932..efaa2f85ca4 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddCyclewayPartSurface.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddCyclewayPartSurface.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.surface
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.OUTDOORS
@@ -36,7 +37,7 @@ class AddCyclewayPartSurface : OsmFilterQuestType<SurfaceAnswer>() {
 
     override fun createForm() = AddPathPartSurfaceForm()
 
-    override fun applyAnswerTo(answer: SurfaceAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SurfaceAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         answer.applyTo(tags, "cycleway")
         answer.updateSegregatedFootAndCycleway(tags)
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddFootwayPartSurface.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddFootwayPartSurface.kt
index 49c06e2052e..e7bdbe7542d 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddFootwayPartSurface.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddFootwayPartSurface.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.surface
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.OUTDOORS
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
@@ -37,7 +38,7 @@ class AddFootwayPartSurface : OsmFilterQuestType<SurfaceAnswer>() {
 
     override fun createForm() = AddPathPartSurfaceForm()
 
-    override fun applyAnswerTo(answer: SurfaceAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SurfaceAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         answer.applyTo(tags, "footway")
         answer.updateSegregatedFootAndCycleway(tags)
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddPathSurface.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddPathSurface.kt
index 671f4c5737c..9ece438a7f4 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddPathSurface.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddPathSurface.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.surface
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.OUTDOORS
@@ -39,7 +40,7 @@ class AddPathSurface : OsmFilterQuestType<SurfaceOrIsStepsAnswer>() {
 
     override fun createForm() = AddPathSurfaceForm()
 
-    override fun applyAnswerTo(answer: SurfaceOrIsStepsAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SurfaceOrIsStepsAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is SurfaceAnswer -> {
                 answer.applyTo(tags)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddPitchSurface.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddPitchSurface.kt
index 8827a488dfc..8aaea19acf4 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddPitchSurface.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddPitchSurface.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.surface
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.OUTDOORS
 import de.westnordost.streetcomplete.osm.Tags
@@ -46,7 +47,7 @@ class AddPitchSurface : OsmFilterQuestType<SurfaceAnswer>() {
 
     override fun createForm() = AddPitchSurfaceForm()
 
-    override fun applyAnswerTo(answer: SurfaceAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SurfaceAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         answer.applyTo(tags)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddRoadSurface.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddRoadSurface.kt
index 61b67a652d5..7cf58b841a8 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddRoadSurface.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddRoadSurface.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.surface
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
@@ -48,7 +49,7 @@ class AddRoadSurface : OsmFilterQuestType<SurfaceAnswer>() {
 
     override fun createForm() = AddRoadSurfaceForm()
 
-    override fun applyAnswerTo(answer: SurfaceAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SurfaceAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         answer.applyTo(tags)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurface.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurface.kt
index 70ae193f886..b5563194ac2 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurface.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurface.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.surface
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.WHEELCHAIR
@@ -37,7 +38,7 @@ class AddSidewalkSurface : OsmFilterQuestType<SidewalkSurfaceAnswer>() {
 
     override fun createForm() = AddSidewalkSurfaceForm()
 
-    override fun applyAnswerTo(answer: SidewalkSurfaceAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: SidewalkSurfaceAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
        when (answer) {
             is SidewalkIsDifferent -> {
                 deleteSmoothnessKeys(Side.LEFT, tags)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingBusStop.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingBusStop.kt
index 7ffc780915f..1739403cd60 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingBusStop.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingBusStop.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.tactile_paving
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BLIND
 import de.westnordost.streetcomplete.osm.Tags
@@ -33,7 +34,7 @@ class AddTactilePavingBusStop : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = TactilePavingForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("tactile_paving", answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingCrosswalk.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingCrosswalk.kt
index 0d56de5f2e7..37dec590599 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingCrosswalk.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingCrosswalk.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.tactile_paving
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -59,7 +60,7 @@ class AddTactilePavingCrosswalk : OsmElementQuestType<TactilePavingCrosswalkAnsw
 
     override fun createForm() = TactilePavingCrosswalkForm()
 
-    override fun applyAnswerTo(answer: TactilePavingCrosswalkAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: TactilePavingCrosswalkAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("tactile_paving", answer.osmValue)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingKerb.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingKerb.kt
index a0de8bfd9a9..da8d31fe90b 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingKerb.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/tactile_paving/AddTactilePavingKerb.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.tactile_paving
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Node
@@ -40,7 +41,7 @@ class AddTactilePavingKerb : OsmElementQuestType<Boolean> {
         if (!eligibleKerbsFilter.matches(element) || element !is Node || !element.couldBeAKerb()) false
         else null
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("tactile_paving", answer.toYesNo())
         if (tags["kerb"] != "no") {
             tags["barrier"] = "kerb"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/toilet_availability/AddToiletAvailability.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/toilet_availability/AddToiletAvailability.kt
index 449405016dd..685124eee7b 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/toilet_availability/AddToiletAvailability.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/toilet_availability/AddToiletAvailability.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.toilet_availability
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
 import de.westnordost.streetcomplete.osm.Tags
@@ -29,7 +30,7 @@ class AddToiletAvailability : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["toilets"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/toilets_fee/AddToiletsFee.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/toilets_fee/AddToiletsFee.kt
index c3350c5b242..0865853aa88 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/toilets_fee/AddToiletsFee.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/toilets_fee/AddToiletsFee.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.toilets_fee
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.quest.AllCountriesExcept
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
@@ -28,7 +29,7 @@ class AddToiletsFee : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["fee"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/tourism_information/AddInformationToTourism.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/tourism_information/AddInformationToTourism.kt
index 7fdd26e4808..4d11a145039 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/tourism_information/AddInformationToTourism.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/tourism_information/AddInformationToTourism.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.tourism_information
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.OUTDOORS
@@ -20,7 +21,7 @@ class AddInformationToTourism : OsmFilterQuestType<TourismInformation>() {
 
     override fun createForm() = AddInformationForm()
 
-    override fun applyAnswerTo(answer: TourismInformation, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: TourismInformation, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["information"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/tracktype/AddTracktype.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/tracktype/AddTracktype.kt
index ceddab2e6e3..9b14dbbadfd 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/tracktype/AddTracktype.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/tracktype/AddTracktype.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.tracktype
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
@@ -30,7 +31,7 @@ class AddTracktype : OsmFilterQuestType<Tracktype>() {
 
     override fun createForm() = AddTracktypeForm()
 
-    override fun applyAnswerTo(answer: Tracktype, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Tracktype, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("tracktype", answer.osmValue)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_calming_type/AddTrafficCalmingType.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_calming_type/AddTrafficCalmingType.kt
index 5591d55f78d..6cb7c1caa91 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_calming_type/AddTrafficCalmingType.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_calming_type/AddTrafficCalmingType.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.traffic_calming_type
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
@@ -19,7 +20,7 @@ class AddTrafficCalmingType : OsmFilterQuestType<TrafficCalmingType>() {
 
     override fun createForm() = AddTrafficCalmingTypeForm()
 
-    override fun applyAnswerTo(answer: TrafficCalmingType, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: TrafficCalmingType, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["traffic_calming"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_button/AddTrafficSignalsButton.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_button/AddTrafficSignalsButton.kt
index ec9ab1add68..335ee42f1e8 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_button/AddTrafficSignalsButton.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_button/AddTrafficSignalsButton.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.traffic_signals_button
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
@@ -31,7 +32,7 @@ class AddTrafficSignalsButton : OsmFilterQuestType<Boolean>() {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["button_operated"] = answer.toYesNo()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_sound/AddTrafficSignalsSound.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_sound/AddTrafficSignalsSound.kt
index 343ce2f7b90..375f764c7d7 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_sound/AddTrafficSignalsSound.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_sound/AddTrafficSignalsSound.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.traffic_signals_sound
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -56,7 +57,7 @@ class AddTrafficSignalsSound : OsmElementQuestType<Boolean> {
 
     override fun createForm() = YesNoQuestForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate(SOUND_SIGNALS, answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_vibrate/AddTrafficSignalsVibration.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_vibrate/AddTrafficSignalsVibration.kt
index 6b4cd968ad3..027a94e04c6 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_vibrate/AddTrafficSignalsVibration.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/traffic_signals_vibrate/AddTrafficSignalsVibration.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.traffic_signals_vibrate
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -60,7 +61,7 @@ class AddTrafficSignalsVibration : OsmElementQuestType<Boolean> {
 
     override fun createForm() = AddTrafficSignalsVibrationForm()
 
-    override fun applyAnswerTo(answer: Boolean, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: Boolean, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate(VIBRATING_BUTTON, answer.toYesNo())
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/way_lit/AddWayLit.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/way_lit/AddWayLit.kt
index bb839bce8b9..2469f78c183 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/way_lit/AddWayLit.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/way_lit/AddWayLit.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.way_lit
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.PEDESTRIAN
 import de.westnordost.streetcomplete.osm.MAXSPEED_TYPE_KEYS
@@ -50,7 +51,7 @@ class AddWayLit : OsmFilterQuestType<WayLitOrIsStepsAnswer>() {
 
     override fun createForm() = WayLitForm()
 
-    override fun applyAnswerTo(answer: WayLitOrIsStepsAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: WayLitOrIsStepsAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         when (answer) {
             is IsActuallyStepsAnswer -> tags["highway"] = "steps"
             is WayLit -> answer.litStatus.applyTo(tags)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessBusiness.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessBusiness.kt
index ced20c30acc..c47dc6cdb1d 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessBusiness.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessBusiness.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.wheelchair_access
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -117,7 +118,7 @@ class AddWheelchairAccessBusiness : OsmFilterQuestType<WheelchairAccess>() {
 
     override fun createForm() = AddWheelchairAccessBusinessForm()
 
-    override fun applyAnswerTo(answer: WheelchairAccess, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: WheelchairAccess, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags["wheelchair"] = answer.osmValue
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessOutside.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessOutside.kt
index 7b216b2512d..a7cc144ff88 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessOutside.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessOutside.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.wheelchair_access
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.RARE
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.WHEELCHAIR
@@ -24,7 +25,7 @@ class AddWheelchairAccessOutside : OsmFilterQuestType<WheelchairAccess>() {
 
     override fun createForm() = AddWheelchairAccessOutsideForm()
 
-    override fun applyAnswerTo(answer: WheelchairAccess, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: WheelchairAccess, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("wheelchair", answer.osmValue)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessPublicTransport.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessPublicTransport.kt
index ec91129ab58..23c14f9819c 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessPublicTransport.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessPublicTransport.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.wheelchair_access
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.WHEELCHAIR
 import de.westnordost.streetcomplete.osm.Tags
@@ -27,7 +28,7 @@ class AddWheelchairAccessPublicTransport : OsmFilterQuestType<WheelchairAccess>(
 
     override fun createForm() = AddWheelchairAccessPublicTransportForm()
 
-    override fun applyAnswerTo(answer: WheelchairAccess, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: WheelchairAccess, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("wheelchair", answer.osmValue)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessToilets.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessToilets.kt
index c5fe8242183..654747c6e78 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessToilets.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessToilets.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.wheelchair_access
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.WHEELCHAIR
 import de.westnordost.streetcomplete.osm.Tags
@@ -27,7 +28,7 @@ class AddWheelchairAccessToilets : OsmFilterQuestType<WheelchairAccess>() {
 
     override fun createForm() = AddWheelchairAccessToiletsForm()
 
-    override fun applyAnswerTo(answer: WheelchairAccess, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: WheelchairAccess, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("wheelchair", answer.osmValue)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessToiletsPart.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessToiletsPart.kt
index 8fc37418354..b00d64e0faf 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessToiletsPart.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/wheelchair_access/AddWheelchairAccessToiletsPart.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.wheelchair_access
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.RARE
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.WHEELCHAIR
@@ -31,7 +32,7 @@ class AddWheelchairAccessToiletsPart : OsmFilterQuestType<WheelchairAccess>() {
 
     override fun createForm() = AddWheelchairAccessToiletsForm()
 
-    override fun applyAnswerTo(answer: WheelchairAccess, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: WheelchairAccess, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         tags.updateWithCheckDate("toilets:wheelchair", answer.osmValue)
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/width/AddCyclewayWidth.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/width/AddCyclewayWidth.kt
index 9ee066b203c..a0670d82d31 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/width/AddCyclewayWidth.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/width/AddCyclewayWidth.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.quests.width
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.BICYCLIST
 import de.westnordost.streetcomplete.osm.Tags
@@ -43,7 +44,7 @@ class AddCyclewayWidth(
 
     override fun createForm() = AddWidthForm()
 
-    override fun applyAnswerTo(answer: WidthAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: WidthAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         val isExclusive = tags["highway"] == "cycleway" && tags["foot"] != "yes" && tags["foot"] != "designated"
 
         val key = if (isExclusive) "width" else "cycleway:width"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/width/AddRoadWidth.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/width/AddRoadWidth.kt
index 875f9512584..0b0337465ec 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/width/AddRoadWidth.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/width/AddRoadWidth.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.quests.width
 
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.filter
@@ -63,7 +64,7 @@ class AddRoadWidth(
 
     override fun createForm() = AddWidthForm()
 
-    override fun applyAnswerTo(answer: WidthAnswer, tags: Tags, timestampEdited: Long) {
+    override fun applyAnswerTo(answer: WidthAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
         val key = if (tags["traffic_calming"] in ROAD_NARROWERS) "maxwidth" else "width"
 
         tags[key] = answer.width.toOsmValue()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/ktx/CountryInfo.kt b/app/src/main/java/de/westnordost/streetcomplete/util/ktx/CountryInfo.kt
index 98d91e6a092..57fafa5c7d9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/util/ktx/CountryInfo.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/ktx/CountryInfo.kt
@@ -98,10 +98,7 @@ val CountryInfo.shoulderLineStyleResId: Int get() = when (edgeLineStyle) {
     else ->                  R.drawable.ic_shoulder_white_line
 }
 
-fun CountryInfo.getExclusiveCycleLaneResId(isLeftHandedTraffic: Boolean): Int =
-    if (isLeftHandedTraffic) exclusiveCycleLaneLeftHandedTrafficResId else exclusiveCycleLaneResId
-
-private val CountryInfo.exclusiveCycleLaneResId: Int get() = when (exclusiveCycleLaneStyle) {
+val CountryInfo.exclusiveCycleLaneResId: Int get() = when (exclusiveCycleLaneStyle) {
     "white" ->                      R.drawable.ic_cycleway_lane_white
     "yellow" ->                     R.drawable.ic_cycleway_lane_yellow
     "white dots" ->                 R.drawable.ic_cycleway_lane_dotted_white
@@ -111,7 +108,7 @@ private val CountryInfo.exclusiveCycleLaneResId: Int get() = when (exclusiveCycl
     else ->                         R.drawable.ic_cycleway_lane_white
 }
 
-private val CountryInfo.exclusiveCycleLaneLeftHandedTrafficResId: Int get() = when (exclusiveCycleLaneStyle) {
+val CountryInfo.exclusiveCycleLaneMirroredResId: Int get() = when (exclusiveCycleLaneStyle) {
     "white" ->                      R.drawable.ic_cycleway_lane_white_l
     "yellow" ->                     R.drawable.ic_cycleway_lane_yellow_l
     "white dots" ->                 R.drawable.ic_cycleway_lane_dotted_white_l
@@ -121,40 +118,39 @@ private val CountryInfo.exclusiveCycleLaneLeftHandedTrafficResId: Int get() = wh
     else ->                         R.drawable.ic_cycleway_lane_white_l
 }
 
-fun CountryInfo.getDualCycleLaneResId(isLeftHandedTraffic: Boolean): Int =
-    if (isLeftHandedTraffic) dualCycleLaneLeftHandedTrafficResId else dualCycleLaneResId
-
-private val CountryInfo.dualCycleLaneResId: Int get() = when {
+val CountryInfo.dualCycleLaneResId: Int get() = when {
     exclusiveCycleLaneStyle.startsWith("white") ->  R.drawable.ic_cycleway_lane_white_dual
     exclusiveCycleLaneStyle.startsWith("yellow") -> R.drawable.ic_cycleway_lane_yellow_dual
     else ->                                         R.drawable.ic_cycleway_lane_white_dual
 }
 
-private val CountryInfo.dualCycleLaneLeftHandedTrafficResId: Int get() = when {
+val CountryInfo.dualCycleLaneMirroredResId: Int get() = when {
     exclusiveCycleLaneStyle.startsWith("white") ->  R.drawable.ic_cycleway_lane_white_dual_l
     exclusiveCycleLaneStyle.startsWith("yellow") -> R.drawable.ic_cycleway_lane_yellow_dual_l
     else ->                                         R.drawable.ic_cycleway_lane_white_dual_l
 }
 
-fun CountryInfo.getAdvisoryCycleLaneResId(isLeftHandedTraffic: Boolean): Int = when (advisoryCycleLaneStyle) {
-    "white dashes" ->
-        if (isLeftHandedTraffic)        R.drawable.ic_cycleway_shared_lane_white_dashed_l
-        else                            R.drawable.ic_cycleway_shared_lane_white_dashed
+val CountryInfo.advisoryCycleLaneResId: Int get() = when (advisoryCycleLaneStyle) {
+    "white dashes" ->                   R.drawable.ic_cycleway_shared_lane_white_dashed
     "white dashes without pictogram" -> R.drawable.ic_cycleway_shared_lane_no_pictograms
     "ochre background" ->               R.drawable.ic_cycleway_shared_lane_orchre_background
     else ->                             R.drawable.ic_cycleway_shared_lane_white_dashed
 }
 
-fun CountryInfo.getPictogramCycleLaneResId(isLeftHandedTraffic: Boolean): Int =
-    if (isLeftHandedTraffic) pictogramCycleLaneLeftHandedTrafficResId else pictogramCycleLaneResId
+val CountryInfo.advisoryCycleLaneMirroredResId: Int get() = when (advisoryCycleLaneStyle) {
+    "white dashes" ->                   R.drawable.ic_cycleway_shared_lane_white_dashed_l
+    "white dashes without pictogram" -> R.drawable.ic_cycleway_shared_lane_no_pictograms
+    "ochre background" ->               R.drawable.ic_cycleway_shared_lane_orchre_background
+    else ->                             R.drawable.ic_cycleway_shared_lane_white_dashed_l
+}
 
-private val CountryInfo.pictogramCycleLaneResId: Int get() = when (pictogramCycleLaneStyle) {
+val CountryInfo.pictogramCycleLaneResId: Int get() = when (pictogramCycleLaneStyle) {
     "white" ->  R.drawable.ic_cycleway_pictograms_white
     "yellow" -> R.drawable.ic_cycleway_pictograms_yellow
     else ->     R.drawable.ic_cycleway_pictograms_white
 }
 
-private val CountryInfo.pictogramCycleLaneLeftHandedTrafficResId: Int get() = when (pictogramCycleLaneStyle) {
+val CountryInfo.pictogramCycleLaneMirroredResId: Int get() = when (pictogramCycleLaneStyle) {
     "white" ->  R.drawable.ic_cycleway_pictograms_white_l
     "yellow" -> R.drawable.ic_cycleway_pictograms_yellow_l
     else ->     R.drawable.ic_cycleway_pictograms_white_l
diff --git a/app/src/main/java/de/westnordost/streetcomplete/view/Text.kt b/app/src/main/java/de/westnordost/streetcomplete/view/Text.kt
index 71424098785..2d9534e4a04 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/view/Text.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/view/Text.kt
@@ -1,5 +1,6 @@
 package de.westnordost.streetcomplete.view
 
+import android.view.Menu
 import android.widget.TextView
 import androidx.annotation.StringRes
 
@@ -25,3 +26,10 @@ fun VerticalLabelView.setText(text: Text?) {
         null -> setText("")
     }
 }
+
+fun Menu.add(groupId: Int, itemId: Int, order: Int, text: Text) {
+    when (text) {
+        is ResText -> add(groupId, itemId, order, text.resId)
+        is CharSequenceText -> add(groupId, itemId, order, text.text)
+    }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/view/controller/StreetSideSelectWithLastAnswerButtonViewController.kt b/app/src/main/java/de/westnordost/streetcomplete/view/controller/StreetSideSelectWithLastAnswerButtonViewController.kt
index 3e5fc1ade11..513ce349f92 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/view/controller/StreetSideSelectWithLastAnswerButtonViewController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/view/controller/StreetSideSelectWithLastAnswerButtonViewController.kt
@@ -128,6 +128,9 @@ class StreetSideSelectWithLastAnswerButtonViewController<I>(
 
     /* ------------------------------------------------------------------------------------------ */
 
+    fun getPuzzleSide(isRight: Boolean) =
+        if (isRight) right else left
+
     fun setPuzzleSide(item: StreetSideDisplayItem<I>?, isRight: Boolean) {
         if (isRight) {
             if (item != null) {
diff --git a/app/src/main/res/drawable/ic_bicycle_white.xml b/app/src/main/res/drawable/ic_bicycle_white.xml
new file mode 100644
index 00000000000..7f1df0718a5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bicycle_white.xml
@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="128dp"
+    android:height="85dp"
+    android:viewportWidth="128"
+    android:viewportHeight="85">
+  <path
+      android:pathData="m50.35,55.69a21.68,21.68 0,0 1,-21.68 21.68,21.68 21.68,0 0,1 -21.68,-21.68 21.68,21.68 0,0 1,21.68 -21.68,21.68 21.68,0 0,1 21.68,21.68m70.64,-0a21.68,21.68 0,0 1,-21.68 21.68,21.68 21.68,0 0,1 -21.68,-21.68 21.68,21.68 0,0 1,21.68 -21.68,21.68 21.68,0 0,1 21.68,21.68m-80.27,-33.39 l27.4,33.27h31.31l-21.53,-33.27h-37.18m37.18,-7.83 l9.54,-0.02m-19.32,41.12 l9.78,-41.1m-48.93,41.1 l16.31,-48.56h15.66"
+      android:strokeLineJoin="round"
+      android:strokeWidth="6.0002"
+      android:fillColor="#00000000"
+      android:strokeColor="#fff"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_cycleway_shoulder.xml b/app/src/main/res/drawable/ic_cycleway_shoulder.xml
new file mode 100644
index 00000000000..d5bb9349134
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cycleway_shoulder.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="256dp"
+    android:height="256dp"
+    android:viewportWidth="256"
+    android:viewportHeight="256">
+  <path
+      android:pathData="M0,0h208v256h-208z"
+      android:fillColor="#808080"/>
+  <path
+      android:pathData="M112,0h16v256h-16z"
+      android:fillColor="#fff"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_fat_footnote.xml b/app/src/main/res/drawable/ic_fat_footnote.xml
deleted file mode 100644
index 85a724e1dd6..00000000000
--- a/app/src/main/res/drawable/ic_fat_footnote.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="96dp"
-    android:height="96dp"
-    android:viewportWidth="96"
-    android:viewportHeight="96">
-  <path
-      android:fillColor="#FF000000"
-      android:pathData="m91.182,48.637 l-25.425,5.9214 18.126,18.279 -20.952,17.506 -14.595,-23.686 -14.125,23.17 -21.658,-16.22 17.42,-19.051 -25.189,-6.4363 7.2977,-26.775 25.425,10.041 -3.2957,-27.547h28.72l-3.5311,28.32 24.483,-10.041z"
-      android:strokeWidth="8"
-      android:strokeColor="#fff"/>
-</vector>
diff --git a/app/src/main/res/drawable/ic_separate_cycleway_allowed.xml b/app/src/main/res/drawable/ic_separate_cycleway_allowed.xml
new file mode 100644
index 00000000000..c8301b8ccd3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_separate_cycleway_allowed.xml
@@ -0,0 +1,31 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="64dp"
+    android:height="64dp"
+    android:viewportWidth="128"
+    android:viewportHeight="128">
+  <path
+      android:pathData="M64,64m-64,0a64,64 0,1 1,128 0a64,64 0,1 1,-128 0"
+      android:fillColor="#808080"/>
+  <path
+      android:pathData="m70.1,16.01c-6.41,0 -11.61,5.2 -11.61,11.61s5.2,11.61 11.61,11.61c6.41,0 11.61,-5.2 11.61,-11.61s-5.2,-11.61 -11.61,-11.61zM57.96,43.45c-11.75,4.39 -20.8,11.86 -20.91,24.27 0,3.18 2.58,5.75 5.76,5.75 3.18,0 5.76,-2.57 5.76,-5.75 0.1,-3.17 1.44,-5.87 3.41,-8.02 0.06,2.83 0.51,6 1.49,9.52 -0.6,1 -0.85,2.5 -0.61,4.69 -2.03,11.69 -14.92,19.29 -23.22,23.07 -2.91,1.29 -4.22,4.69 -2.93,7.6 1.29,2.91 4.69,4.22 7.6,2.93 11.02,-5.74 25.04,-15 29.08,-27.16 9.16,4.27 17.73,13.86 19.27,22.71 0.45,3.15 3.37,5.33 6.51,4.88 3.15,-0.45 5.33,-3.36 4.89,-6.51 -2.39,-14.21 -12.2,-24.72 -24.43,-30.88 -3.54,-1.52 -3.82,-9.79 -3.62,-12.78 10.73,6.09 21.74,8.82 32.63,3.47 2.84,-1.42 3.99,-4.88 2.57,-7.72 -1.42,-2.84 -4.88,-4 -7.72,-2.57 -12.8,6.33 -20.77,-3.36 -27.51,-7.29 -2.41,-1.69 -5.61,-1.69 -8.01,-0.22z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="M71.59,66L116.41,66A9.59,9.59 0,0 1,126 75.59L126,116.41A9.59,9.59 0,0 1,116.41 126L71.59,126A9.59,9.59 0,0 1,62 116.41L62,75.59A9.59,9.59 0,0 1,71.59 66z"
+      android:strokeLineJoin="round"
+      android:strokeWidth="4"
+      android:fillColor="#fff"
+      android:strokeColor="#000"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="m88.25,94.41a9.32,9.28 0,0 1,-9.32 9.28,9.32 9.28,0 0,1 -9.32,-9.28 9.32,9.28 0,0 1,9.32 -9.28,9.32 9.28,0 0,1 9.32,9.28zM118.61,94.41a9.32,9.28 0,0 1,-9.32 9.28,9.32 9.28,0 0,1 -9.32,-9.28 9.32,9.28 0,0 1,9.32 -9.28,9.32 9.28,0 0,1 9.32,9.28zM84.11,80.12 L95.89,94.36h13.46l-9.25,-14.24h-15.98m11.78,14.24 l4.21,-17.59m0,0 l4.1,-0.02m-25.13,17.6 l7.01,-20.78h6.73"
+      android:strokeLineJoin="round"
+      android:strokeWidth="3"
+      android:strokeColor="#000"
+      android:strokeLineCap="round"/>
+  <path
+      android:fillColor="#000"
+      android:pathData="m94,114c0,3 -1.99,6 -6,6s-6.06,-2.95 -6,-6c0.06,-3.05 1.9,-6 6,-6s6,3 6,6zM91,114c0.01,-1.46 -0.99,-3 -3,-3s-3,1.49 -3,3 1,3 3,3 2.99,-1.54 3,-3z" />
+  <path
+      android:fillColor="#000"
+      android:pathData="m106.98,120.01l-3.28,0l-3.7,-5 -1,1 0,4 -3,-0 0,-12l3,0l-0,5 5,-5l3.3,0l-5.3,5.2z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_separate_cycleway_exclusive.xml b/app/src/main/res/drawable/ic_separate_cycleway_exclusive.xml
new file mode 100644
index 00000000000..36a91e629f9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_separate_cycleway_exclusive.xml
@@ -0,0 +1,8 @@
+<vector android:height="64dp" android:viewportHeight="128"
+    android:viewportWidth="128" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#808080" android:pathData="M64,64m-64,0a64,64 0,1 1,128 0a64,64 0,1 1,-128 0"/>
+    <path android:fillColor="#00000000"
+        android:pathData="m51.79,72.39a19.32,19.41 90,0 1,-19.41 19.32,19.32 19.41,90 0,1 -19.41,-19.32 19.32,19.41 90,0 1,19.41 -19.32,19.32 19.41,90 0,1 19.41,19.32zM115.03,72.39a19.32,19.41 90,0 1,-19.41 19.32,19.32 19.41,90 0,1 -19.41,-19.32 19.32,19.41 90,0 1,19.41 -19.32,19.32 19.41,90 0,1 19.41,19.32zM43.17,42.62 L67.69,72.28l28.03,0l-19.27,-29.66l-33.29,0m24.53,29.66 l8.76,-36.63m0,0 l8.54,-0.03m-52.34,36.65 l14.6,-43.29l14.02,0"
+        android:strokeColor="#fff" android:strokeLineCap="round"
+        android:strokeLineJoin="round" android:strokeWidth="6"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_separate_cycleway_no.xml b/app/src/main/res/drawable/ic_separate_cycleway_no.xml
new file mode 100644
index 00000000000..821874a99b6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_separate_cycleway_no.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="64dp"
+    android:height="64dp"
+    android:viewportWidth="128"
+    android:viewportHeight="128">
+  <path
+      android:pathData="M64,64m-64,0a64,64 0,1 1,128 0a64,64 0,1 1,-128 0"
+      android:fillColor="#808080"/>
+  <path
+      android:pathData="m70.1,16c-6.41,0 -11.61,5.2 -11.61,11.61s5.2,11.61 11.61,11.61c6.41,0 11.61,-5.2 11.61,-11.61s-5.2,-11.61 -11.61,-11.61zM57.96,43.44c-11.75,4.39 -20.8,11.86 -20.91,24.28 0,3.18 2.58,5.75 5.76,5.75 3.18,0 5.76,-2.57 5.76,-5.75 0.1,-3.17 1.44,-5.87 3.41,-8.02 0.06,2.83 0.51,6 1.49,9.52 -0.6,1 -0.85,2.5 -0.61,4.69 -2.03,11.69 -14.92,19.29 -23.22,23.07 -2.91,1.29 -4.22,4.69 -2.93,7.6 1.29,2.91 4.69,4.22 7.6,2.93 11.02,-5.74 25.04,-15 29.08,-27.16 9.16,4.27 17.73,13.86 19.27,22.71 0.45,3.15 3.37,5.33 6.51,4.88 3.15,-0.45 5.33,-3.36 4.89,-6.51 -2.39,-14.21 -12.2,-24.72 -24.43,-30.88 -3.54,-1.52 -3.82,-9.79 -3.62,-12.78 10.73,6.09 21.74,8.82 32.63,3.47 2.84,-1.42 3.99,-4.88 2.57,-7.72 -1.42,-2.84 -4.88,-4 -7.72,-2.57 -12.8,6.33 -20.77,-3.36 -27.51,-7.29 -2.41,-1.69 -5.61,-1.69 -8.01,-0.22z"
+      android:fillColor="#fff"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_path_segregated_no.xml b/app/src/main/res/drawable/ic_separate_cycleway_not_segregated.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_path_segregated_no.xml
rename to app/src/main/res/drawable/ic_separate_cycleway_not_segregated.xml
diff --git a/app/src/main/res/drawable/ic_path_segregated.xml b/app/src/main/res/drawable/ic_separate_cycleway_segregated.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_path_segregated.xml
rename to app/src/main/res/drawable/ic_separate_cycleway_segregated.xml
diff --git a/app/src/main/res/drawable/ic_path_segregated_l.xml b/app/src/main/res/drawable/ic_separate_cycleway_segregated_l.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_path_segregated_l.xml
rename to app/src/main/res/drawable/ic_separate_cycleway_segregated_l.xml
diff --git a/app/src/main/res/drawable/ic_separate_cycleway_with_sidewalk.xml b/app/src/main/res/drawable/ic_separate_cycleway_with_sidewalk.xml
new file mode 100644
index 00000000000..500a9dfdb5f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_separate_cycleway_with_sidewalk.xml
@@ -0,0 +1,20 @@
+<vector android:height="64dp" android:viewportHeight="128"
+    android:viewportWidth="128" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#808080"
+        android:pathData="M0,64a64,64 0,1 0,128 0a64,64 0,1 0,-128 0z" android:strokeWidth="3.7796"/>
+    <group>
+        <clip-path android:pathData="M0,64a64,64 0,1 0,128 0a64,64 0,1 0,-128 0z"/>
+        <path android:fillColor="#aaa" android:pathData="m76,0h52v128h-52z"/>
+        <path android:fillColor="#00000000"
+            android:pathData="m76,88h16m-16,-24h16m-16,-48h16m-16,24h16m-16,72h16m16,-24h16m-16,-24h16m-16,-48h16m-16,24h16m-16,72h16m-32,12h16m-16,-48h16m-16,-24h16m-16,-48h16m-16,24h16m-16,72h16m16,-100v128m-16,0v-128m-16,0v128m32,-100.58h4m-4,24h4m-4,72h4m-4,-24h4m-4,-24h4m-4,-72h4"
+            android:strokeColor="#999" android:strokeWidth="4"/>
+        <path android:fillColor="#999" android:pathData="m68,0h8v128h-8z"/>
+        <path android:fillColor="#666" android:pathData="m60,0h8v128h-8z"/>
+    </group>
+    <path android:fillColor="#fff"
+        android:pathData="m101.49,45.12c-2.78,0 -5.03,2.25 -5.03,5.03 0,2.78 2.25,5.03 5.03,5.03 2.78,0 5.03,-2.25 5.03,-5.03 0,-2.78 -2.25,-5.03 -5.03,-5.03zM96.23,57.01c-5.09,1.9 -9.01,5.14 -9.06,10.52 0,1.38 1.12,2.49 2.49,2.49 1.38,0 2.49,-1.12 2.49,-2.49 0.04,-1.37 0.62,-2.54 1.48,-3.47 0.03,1.22 0.22,2.6 0.64,4.13 -0.26,0.43 -0.37,1.08 -0.27,2.03 -0.88,5.06 -6.46,8.36 -10.06,9.99 -1.26,0.56 -1.83,2.03 -1.27,3.29 0.56,1.26 2.03,1.83 3.29,1.27 4.78,-2.49 10.85,-6.5 12.6,-11.77 3.97,1.85 7.68,6 8.35,9.84 0.19,1.36 1.46,2.31 2.82,2.12 1.36,-0.19 2.31,-1.46 2.12,-2.82 -1.03,-6.15 -5.28,-10.71 -10.58,-13.38 -1.54,-0.66 -1.66,-4.24 -1.57,-5.54 4.65,2.64 9.42,3.82 14.14,1.5 1.23,-0.62 1.73,-2.11 1.11,-3.35 -0.62,-1.23 -2.11,-1.73 -3.34,-1.11 -5.55,2.74 -9,-1.46 -11.92,-3.16 -1.04,-0.73 -2.43,-0.73 -3.47,-0.09z" android:strokeWidth=".1264"/>
+    <path android:fillColor="#00000000"
+        android:pathData="m24.95,68.53a8.07,8.04 0,0 1,-8.07 8.04,8.07 8.04,0 0,1 -8.07,-8.04 8.07,8.04 0,0 1,8.07 -8.04,8.07 8.04,0 0,1 8.07,8.04zM51.25,68.53a8.07,8.04 0,0 1,-8.07 8.04,8.07 8.04,0 0,1 -8.07,-8.04 8.07,8.04 0,0 1,8.07 -8.04,8.07 8.04,0 0,1 8.07,8.04zM21.37,56.15 L31.57,68.48h11.66l-8.01,-12.33h-13.84m10.2,12.33 l3.64,-15.23m0,0 l3.55,-0.01m-21.76,15.24 l6.07,-18h5.83"
+        android:strokeColor="#fff" android:strokeLineCap="round"
+        android:strokeLineJoin="round" android:strokeWidth="3.2418"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_separate_cycleway_with_sidewalk_l.xml b/app/src/main/res/drawable/ic_separate_cycleway_with_sidewalk_l.xml
new file mode 100644
index 00000000000..b94e4f20b20
--- /dev/null
+++ b/app/src/main/res/drawable/ic_separate_cycleway_with_sidewalk_l.xml
@@ -0,0 +1,20 @@
+<vector android:height="64dp" android:viewportHeight="128"
+    android:viewportWidth="128" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#808080"
+        android:pathData="M0,64a64,64 0,1 0,128 0a64,64 0,1 0,-128 0z" android:strokeWidth="3.7796"/>
+    <group>
+        <clip-path android:pathData="M128,64a64,64 0,1 1,-128 0a64,64 0,1 1,128 0z"/>
+        <path android:fillColor="#aaa" android:pathData="m52,0l-52,0l-0,128l52,0z"/>
+        <path android:fillColor="#00000000"
+            android:pathData="m52,88l-16,0m16,-24l-16,0m16,-48l-16,0m16,24l-16,0m16,72l-16,0m-16,-24l-16,0m16,-24l-16,0m16,-48l-16,0m16,24l-16,0m16,72l-16,0m32,12l-16,0m16,-48l-16,0m16,-24l-16,0m16,-48l-16,0m16,24l-16,0m16,72l-16,0m-16,-100l-0,128m16,0l-0,-128m16,0l-0,128m-32,-100.58l-4,0m4,24l-4,0m4,72l-4,0m4,-24l-4,0m4,-24l-4,0m4,-72l-4,0"
+            android:strokeColor="#999" android:strokeWidth="4"/>
+        <path android:fillColor="#999" android:pathData="m60,0l-8,0l-0,128l8,0z"/>
+        <path android:fillColor="#666" android:pathData="m68,0l-8,0l-0,128l8,0z"/>
+    </group>
+    <path android:fillColor="#fff"
+        android:pathData="m33.49,45.12c-2.78,0 -5.03,2.25 -5.03,5.03 0,2.78 2.25,5.03 5.03,5.03 2.78,0 5.03,-2.25 5.03,-5.03 0,-2.78 -2.25,-5.03 -5.03,-5.03zM28.23,57.01c-5.09,1.9 -9.01,5.14 -9.06,10.52 0,1.38 1.12,2.49 2.49,2.49 1.38,0 2.49,-1.12 2.49,-2.49 0.04,-1.37 0.62,-2.54 1.48,-3.47 0.03,1.22 0.22,2.6 0.64,4.13 -0.26,0.43 -0.37,1.08 -0.27,2.03 -0.88,5.06 -6.46,8.36 -10.06,9.99 -1.26,0.56 -1.83,2.03 -1.27,3.29 0.56,1.26 2.03,1.83 3.29,1.27 4.78,-2.49 10.85,-6.5 12.6,-11.77 3.97,1.85 7.68,6 8.35,9.84 0.19,1.36 1.46,2.31 2.82,2.12 1.36,-0.19 2.31,-1.46 2.12,-2.82 -1.03,-6.15 -5.28,-10.71 -10.58,-13.38 -1.54,-0.66 -1.66,-4.24 -1.57,-5.54 4.65,2.64 9.42,3.82 14.14,1.5 1.23,-0.62 1.73,-2.11 1.11,-3.35 -0.62,-1.23 -2.11,-1.73 -3.34,-1.11 -5.55,2.74 -9,-1.46 -11.92,-3.16 -1.04,-0.73 -2.43,-0.73 -3.47,-0.09z" android:strokeWidth=".1264"/>
+    <path android:fillColor="#00000000"
+        android:pathData="m92.95,68.53a8.07,8.04 0,0 1,-8.07 8.04,8.07 8.04,0 0,1 -8.07,-8.04 8.07,8.04 0,0 1,8.07 -8.04,8.07 8.04,0 0,1 8.07,8.04zM119.25,68.53a8.07,8.04 0,0 1,-8.07 8.04,8.07 8.04,0 0,1 -8.07,-8.04 8.07,8.04 0,0 1,8.07 -8.04,8.07 8.04,0 0,1 8.07,8.04zM89.37,56.15 L99.57,68.48h11.66l-8.01,-12.33h-13.84m10.2,12.33 l3.64,-15.23m0,0 l3.55,-0.01m-21.76,15.24 l6.07,-18h5.83"
+        android:strokeColor="#fff" android:strokeLineCap="round"
+        android:strokeLineJoin="round" android:strokeWidth="3.2418"/>
+</vector>
diff --git a/app/src/main/res/layout/cell_labeled_icon_select_right.xml b/app/src/main/res/layout/cell_labeled_icon_select_right.xml
index 56e6e24d385..e318708ebc4 100644
--- a/app/src/main/res/layout/cell_labeled_icon_select_right.xml
+++ b/app/src/main/res/layout/cell_labeled_icon_select_right.xml
@@ -16,7 +16,7 @@
         android:layout_height="wrap_content"
         android:layout_marginEnd="6dp"
         android:scaleType="fitCenter"
-        tools:src="@drawable/ic_path_segregated_no"
+        tools:src="@drawable/ic_separate_cycleway_not_segregated"
         />
 
     <TextView
diff --git a/app/src/main/res/layout/fragment_overlay_image_select.xml b/app/src/main/res/layout/fragment_overlay_image_select.xml
index b2aa574f84b..9c7484da38a 100644
--- a/app/src/main/res/layout/fragment_overlay_image_select.xml
+++ b/app/src/main/res/layout/fragment_overlay_image_select.xml
@@ -9,6 +9,8 @@
         android:id="@+id/selectButton"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
+        android:layout_marginLeft="16dp"
+        android:layout_marginRight="16dp"
         android:background="@drawable/background_button_selectable_outline"
         android:animateLayoutChanges="true"
         android:layout_centerInParent="true">
diff --git a/app/src/main/res/layout/sign_bicycle_boulevard.xml b/app/src/main/res/layout/sign_bicycle_boulevard.xml
new file mode 100644
index 00000000000..d80d147ed83
--- /dev/null
+++ b/app/src/main/res/layout/sign_bicycle_boulevard.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/signBicycleBoulevard"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/background"
+    android:clickable="true"
+    android:gravity="center"
+    android:focusable="true">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:background="@drawable/background_rectangular_sign_blue"
+        android:showDividers="middle"
+        android:divider="@drawable/space_8dp"
+        android:padding="16dp"
+        android:gravity="center">
+
+        <ImageView
+            android:id="@+id/bicycleIconView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/ic_bicycle_white"/>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/bicycle_boulevard"
+            android:textColor="@color/traffic_white"
+            android:textStyle="bold"
+            android:textSize="18dp"
+            tools:ignore="SpUsage" />
+
+    </LinearLayout>
+
+</RelativeLayout>
diff --git a/app/src/main/res/raw/changelog.yml b/app/src/main/res/raw/changelog.yml
index 0875b45f72d..b10dd1b7ddf 100644
--- a/app/src/main/res/raw/changelog.yml
+++ b/app/src/main/res/raw/changelog.yml
@@ -1,12 +1,22 @@
 v50.0-beta1: |
-  <h3>🚚📦 Move node (#3502, #4579), by @Helium314</h3>
+  <h3>🚲️️ Cycleway Overlay (#4657, #4233)</h3>
+  <p>There is a new overlay which allows you to review and edit the streetside cycleway infrastructure as well as whether there is any segregation on combined foot- and cycle paths.</p>
+  <h3>🚚 Move node (#3502, #4579), by @Helium314</h3>
   <p>"It’s at slightly different position…" is a new generic answer available for every POI-based
       quest and overlay. It lets you correct the position of the POI.</p>
   </ul>
+  <h3>Enhanced Overlays</h3>
+  <ul>
+  <li>Sidewalks: Missing sidewalk information on unpaved roads and on roads with a very low speed limit are not shown in red</li>
+  <li>Sidewalks: Support sidewalks mapped on exclusive cycleways</li>
+  <li>Street parking: Fix a crash when editing partially (#4634)</li>
+  <li>Other small enhancements (see #4657)</li>
+  </ul>
   <h3>Enhanced Quests</h3>
   <ul>
   <li>Surfaces for segregated foot and cycleway: Correct common surface (#4615), by @mnalis</li>
-  <li>Other small enhancements (#4637), thanks @matkoniecz</li>
+  <li>Disabled cycleway quest by default because it can be done more comprehensively in the overlay</li>
+  <li>Other small enhancements (#4637, ...), thanks @matkoniecz</li>
   </ul>
 
 v49.2: |
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 127b10f121b..94b7429c083 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -940,6 +940,7 @@ Before uploading your changes, the app checks with a &lt;a href=\"https://www.we
     <string name="quest_cycleway_value_track_dual">sidepath or protected bicycle lane, both directions</string>
     <string name="quest_cycleway_value_none_but_no_oneway">none, but cyclists may use road in both directions</string>
     <string name="quest_cycleway_value_none_and_oneway">none, no cycling allowed in this direction</string>
+    <string name="quest_cycleway_value_shoulder">none, but shoulder is usable for cyclists</string>
     <string name="quest_cycleway_answer_contraflow_cycleway">Also bicycle path on the other side…</string>
     <string name="quest_cycleway_answer_no_bicycle_infrastructure">No bicycle lane or path at all</string>
     <string name="quest_cycleway_answer_no_bicycle_infrastructure_explanation">As usual, tap on one side of the illustration, select a proper answer and do the same for the other side.\n\nNote all the available options: some cases are not obvious, like a oneway road that allows cyclists in both directions!</string>
@@ -1469,29 +1470,6 @@ Otherwise, you can download another keyboard in the app store. Popular keyboards
     <string name="quest_streetName_menuItem_nolanguage">(unspecified)</string>
     <string name="quest_streetName_menuItem_international">international</string>
 
-    <string name="select_street_parking_orientation">"Select orientation toward roadway:"</string>
-    <string name="select_street_parking_position">"Select position:"</string>
-    <string name="select_street_parking_no">"Any sign or road marking here?"</string>
-    <string name="street_parking_parallel">"Parallel"</string>
-    <string name="street_parking_diagonal">"Diagonal"</string>
-    <string name="street_parking_perpendicular">"Perpendicular"</string>
-    <string name="street_parking_on_street">"On street"</string>
-    <string name="street_parking_half_on_kerb">"Partially on street"</string>
-    <string name="street_parking_on_kerb">"Adjacent to street"</string>
-    <string name="street_parking_street_side">"Adjacent to street only in dedicated spaces"</string>
-    <string name="street_parking_painted_area_only">"On street only in dedicated spaces"</string>
-    <string name="street_parking_separate">Parking displayed separately on map</string>
-    <string name="street_parking_no">No parking here / Not allowed</string>
-    <string name="street_parking_prohibited">No parking</string>
-    <string name="street_stopping_prohibited">No stopping</string>
-    <string name="street_standing_prohibited">No standing</string>
-    <string name="street_parking_conditional_restrictions">Parking / Stopping restricted conditionally</string>
-    <string name="street_parking_no_other_reasons">No explicit sign / parking not possible</string>
-    <string name="street_parking_conditional_restrictions_hint">"If it is at all possible to park or stop here, please answer how those who might park here can do so. Which restrictions apply exactly can be specified later.
-
-Alternatively, you can leave a note (with a photo)."</string>
-    <string name="street_parking_street_width">Street width: %s</string>
-
     <string name="quest_streetSurface_title">"What surface does this piece of road have?"</string>
     <string name="quest_streetSurface_square_title">"What surface does this square have?"</string>
 
@@ -1594,6 +1572,21 @@ Partially means that a wheelchair can enter and use the restroom, but no handrai
 
     <string name="select_overlay">Select Overlay</string>
 
+    <string name="overlay_cycleway">Cycleways</string>
+    <string name="bicycle_boulevard_is_a">It’s a %1$s</string>
+    <string name="bicycle_boulevard_is_not_a">It’s not a %1$s</string>
+    <string name="bicycle_boulevard">Bicycle boulevard</string>
+    <string name="separate_cycleway_no">No cycling</string>
+    <string name="separate_cycleway_allowed">Cyclists are allowed</string>
+    <string name="separate_cycleway_no_or_allowed">Not designated for cyclists (regardless of whether cycling is allowed or not)</string>
+    <string name="separate_cycleway_non_segregated">Designated combined foot- and cycleway</string>
+    <string name="separate_cycleway_segregated">Segregated cycle- and footway</string>
+    <string name="separate_cycleway_exclusive">Exclusive cycleway</string>
+    <string name="separate_cycleway_with_sidewalk">Exclusive cycleway which has a sidewalk for foot traffic (with kerb)</string>
+    <string name="cycleway_reverse_direction">Reverse cycleway direction</string>
+    <string name="cycleway_reverse_direction_warning">That a cycleway goes into the other direction is extremely rare.</string>
+    <string name="cycleway_reverse_direction_toast">Tap which side to reverse</string>
+
     <string name="overlay_none">None</string>
 
     <string name="overlay_lit">Street lighting</string>
@@ -1617,4 +1610,28 @@ Partially means that a wheelchair can enter and use the restroom, but no handrai
     <string name="confirmation_replace_shop_no">"Still same place"</string>
 
     <string name="overlay_street_parking">Street parking</string>
+
+    <string name="select_street_parking_orientation">"Select orientation toward roadway:"</string>
+    <string name="select_street_parking_position">"Select position:"</string>
+    <string name="select_street_parking_no">"Any sign or road marking here?"</string>
+    <string name="street_parking_parallel">"Parallel"</string>
+    <string name="street_parking_diagonal">"Diagonal"</string>
+    <string name="street_parking_perpendicular">"Perpendicular"</string>
+    <string name="street_parking_on_street">"On street"</string>
+    <string name="street_parking_half_on_kerb">"Partially on street"</string>
+    <string name="street_parking_on_kerb">"Adjacent to street"</string>
+    <string name="street_parking_street_side">"Adjacent to street only in dedicated spaces"</string>
+    <string name="street_parking_painted_area_only">"On street only in dedicated spaces"</string>
+    <string name="street_parking_separate">Parking displayed separately on map</string>
+    <string name="street_parking_no">No parking here / Not allowed</string>
+    <string name="street_parking_prohibited">No parking</string>
+    <string name="street_stopping_prohibited">No stopping</string>
+    <string name="street_standing_prohibited">No standing</string>
+    <string name="street_parking_conditional_restrictions">Parking / Stopping restricted conditionally</string>
+    <string name="street_parking_no_other_reasons">No explicit sign / parking not possible</string>
+    <string name="street_parking_conditional_restrictions_hint">"If it is at all possible to park or stop here, please answer how those who might park here can do so. Which restrictions apply exactly can be specified later.
+
+Alternatively, you can leave a note (with a photo)."</string>
+    <string name="street_parking_street_width">Street width: %s</string>
+
 </resources>
diff --git a/res/graphics/cycleway segregation/allowed.svg b/res/graphics/cycleway segregation/allowed.svg
new file mode 100644
index 00000000000..73295bba8ef
--- /dev/null
+++ b/res/graphics/cycleway segregation/allowed.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
+ <g transform="translate(0,-229.27)">
+  <circle cx="64" cy="293.27" r="64" fill="#808080" stroke-width="3.7796"/>
+  <path d="m70.101 245.28c-6.4108 0-11.61 5.1981-11.61 11.61s5.1992 11.608 11.61 11.608c6.4118 0 11.61-5.1959 11.61-11.608s-5.1981-11.61-11.61-11.61zm-12.145 27.441c-11.746 4.3932-20.804 11.858-20.906 24.275 0 3.179 2.5765 5.7534 5.7556 5.7534 3.1796 0 5.7567-2.5745 5.7572-5.7534 0.09543-3.1737 1.441-5.8682 3.4077-8.0207 0.06172 2.8276 0.51294 6.0018 1.4858 9.5234-0.59911 0.99744-0.84931 2.4985-0.61326 4.6933-2.0311 11.688-14.917 19.293-23.224 23.066-2.9056 1.291-4.2158 4.694-2.9251 7.5994 1.2919 2.9053 4.694 4.216 7.5993 2.9251 11.023-5.7412 25.044-15.001 29.081-27.161 9.1572 4.269 17.726 13.86 19.27 22.712 0.44877 3.1474 3.3667 5.3317 6.5142 4.8829 3.1474-0.44984 5.3347-3.3625 4.885-6.51-2.3875-14.206-12.196-24.718-24.431-30.878-3.544-1.5225-3.8245-9.7947-3.6248-12.778 10.734 6.0939 21.739 8.8152 32.63 3.471 2.8441-1.4213 3.9944-4.8804 2.5732-7.7237-1.4222-2.8432-4.8761-3.9954-7.7194-2.5732-12.801 6.3282-20.773-3.3625-27.508-7.2851-2.4093-1.6878-5.612-1.6929-8.0062-0.21921z" fill="#fff" stroke-width="5.3611"/>
+  <rect x="62" y="295.27" width="64" height="60" ry="9.588" fill="#fff" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.9996"/>
+  <path d="m88.249 323.68a9.317 9.2773 0 0 1-9.317 9.2773 9.317 9.2773 0 0 1-9.317-9.2773 9.317 9.2773 0 0 1 9.317-9.2772 9.317 9.2773 0 0 1 9.317 9.2772zm30.363-8.3e-4a9.317 9.2773 0 0 1-9.317 9.2769 9.317 9.2773 0 0 1-9.317-9.2769 9.317 9.2773 0 0 1 9.317-9.2773 9.317 9.2773 0 0 1 9.317 9.2773zm-34.501-14.289 11.776 14.238h13.458l-9.2524-14.238h-15.982m11.776 14.238 4.2058-17.588m0 0 4.0995-0.0151m-25.128 17.597 7.0107-20.783h6.7291" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.0034"/>
+  <g transform="matrix(3.7795 0 0 3.7795 0 -637.25)" stroke-width=".26459" aria-label="OK">
+   <path d="m24.871 259.43c3.1e-5 0.79321-0.52524 1.5875-1.5875 1.5875s-1.6021-0.78094-1.5875-1.5875c0.01463-0.80657 0.50257-1.5875 1.5875-1.5875s1.5875 0.79431 1.5875 1.5875zm-0.79376 0c0.0032-0.38686-0.26102-0.79376-0.79376-0.79376s-0.79376 0.39421-0.79376 0.79376 0.2642 0.79375 0.79376 0.79375 0.79059-0.40689 0.79376-0.79375z"/>
+   <path d="m28.305 261.02h-0.8677l-0.97897-1.3229-0.26459 0.26459 2e-6 1.0583-0.79376-4e-5 1e-6 -3.175h0.79376l-2e-6 1.323 1.3229-1.3229h0.87313l-1.4023 1.3758z"/>
+  </g>
+ </g>
+</svg>
diff --git a/res/graphics/cycleway segregation/exclusive.svg b/res/graphics/cycleway segregation/exclusive.svg
new file mode 100644
index 00000000000..a471aa3ec97
--- /dev/null
+++ b/res/graphics/cycleway segregation/exclusive.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
+ <g transform="matrix(3.7796 0 0 3.7796 0 -866.54)">
+  <circle cx="16.933" cy="246.2" r="16.933" fill="#808080"/>
+  <path d="m13.702 248.42a5.1345 5.1126 0 0 1-5.1345 5.1126 5.1345 5.1126 0 0 1-5.1345-5.1126 5.1345 5.1126 0 0 1 5.1345-5.1126 5.1345 5.1126 0 0 1 5.1345 5.1126zm16.732-4.8e-4a5.1345 5.1126 0 0 1-5.1345 5.1124 5.1345 5.1126 0 0 1-5.1345-5.1124 5.1345 5.1126 0 0 1 5.1345-5.1126 5.1345 5.1126 0 0 1 5.1345 5.1126zm-19.013-7.8743 6.4894 7.8464h7.4166l-5.0989-7.8464h-8.8072m6.4894 7.8464 2.3177-9.6925m0 0 2.2592-7e-3m-13.848 9.6967 3.8635-11.453h3.7083" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.574"/>
+ </g>
+</svg>
diff --git a/res/graphics/cycleway segregation/no.svg b/res/graphics/cycleway segregation/no.svg
new file mode 100644
index 00000000000..d5f3fd7c921
--- /dev/null
+++ b/res/graphics/cycleway segregation/no.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
+ <g transform="matrix(3.7796 0 0 3.7796 0 -866.54)">
+  <circle cx="16.933" cy="246.2" r="16.933" fill="#808080"/>
+  <path d="m18.548 233.5c-1.6962 0-3.0718 1.3754-3.0718 3.0718s1.3756 3.0712 3.0718 3.0712c1.6965 0 3.0718-1.3748 3.0718-3.0712s-1.3753-3.0718-3.0718-3.0718zm-3.2134 7.2605c-3.1079 1.1624-5.5043 3.1374-5.5314 6.4229 0 0.84111 0.6817 1.5223 1.5229 1.5223 0.84129 0 1.5231-0.68118 1.5233-1.5223 0.02525-0.83971 0.38127-1.5526 0.90163-2.1222 0.01633 0.74814 0.13572 1.588 0.39311 2.5198-0.15852 0.26391-0.22472 0.66106-0.16226 1.2418-0.5374 3.0925-3.9469 5.1046-6.1448 6.1029-0.76878 0.34157-1.1155 1.242-0.77394 2.0107 0.34181 0.7687 1.242 1.1155 2.0107 0.77395 2.9166-1.519 6.6263-3.9692 7.6943-7.1864 2.4229 1.1295 4.6901 3.6672 5.0987 6.0092 0.11874 0.83275 0.89078 1.4107 1.7236 1.292 0.83275-0.11902 1.4115-0.88967 1.2925-1.7224-0.6317-3.7588-3.227-6.5401-6.4642-8.1699-0.93769-0.40283-1.0119-2.5915-0.95906-3.3808 2.8402 1.6124 5.7519 2.3324 8.6333 0.91837 0.75251-0.37605 1.0569-1.2913 0.68083-2.0436-0.37629-0.75227-1.2901-1.0571-2.0424-0.68084-3.3869 1.6744-5.4961-0.88966-7.2783-1.9275-0.63747-0.44657-1.4848-0.44792-2.1183-0.058z" fill="#fff" stroke-width="1.4185"/>
+ </g>
+</svg>
diff --git a/res/graphics/cycleway segregation/segregated_r.svg b/res/graphics/cycleway segregation/segregated_r.svg
index c2e45b4d74a..c1c4e9c8f79 100644
--- a/res/graphics/cycleway segregation/segregated_r.svg
+++ b/res/graphics/cycleway segregation/segregated_r.svg
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<svg width="128" height="128" version="1.1" viewBox="0 0 33.867 33.867" xmlns="http://www.w3.org/2000/svg">
- <circle cx="16.933" cy="16.93" r="16.933" fill="#808080"/>
- <path d="m26.856 11.936c-0.73485 0-1.3309 0.59588-1.3309 1.3309 0 0.73499 0.59601 1.3306 1.3309 1.3306 0.73499 0 1.3309-0.59564 1.3309-1.3306 0-0.73499-0.59588-1.3309-1.3309-1.3309zm-1.3922 3.1456c-1.3465 0.50359-2.3848 1.3593-2.3965 2.7828 0 0.36442 0.29535 0.65953 0.65976 0.65953 0.36448 0 0.65993-0.29513 0.66-0.65953 0.01094-0.36381 0.16518-0.67267 0.39064-0.91945 0.0071 0.32413 0.0588 0.68799 0.17031 1.0917-0.06867 0.11433-0.09735 0.28641-0.0703 0.53801-0.23283 1.3398-1.71 2.2116-2.6622 2.6441-0.33308 0.14798-0.48326 0.53808-0.33532 0.87115 0.14809 0.33305 0.53808 0.48329 0.87115 0.33532 1.2637-0.65812 2.8709-1.7197 3.3336-3.1135 1.0497 0.48938 2.032 1.5888 2.209 2.6035 0.05145 0.3608 0.38592 0.6112 0.74673 0.55975 0.3608-0.05155 0.61153-0.38545 0.55999-0.74626-0.27369-1.6285-1.3981-2.8335-2.8006-3.5398-0.40626-0.17452-0.4384-1.1228-0.41552-1.4647 1.2305 0.69856 2.492 1.0105 3.7405 0.3979 0.32603-0.16292 0.4579-0.55945 0.29497-0.8854-0.16303-0.32592-0.55895-0.458-0.8849-0.29497-1.4674 0.72542-2.3812-0.38546-3.1533-0.83513-0.27619-0.19348-0.6433-0.19406-0.91778-0.02512z" fill="#fff" stroke-width=".033448"/>
- <rect x="15.875" y="4.2725e-6" width="2.1167" height="33.867" fill="#fff"/>
- <path d="m6.6028 18.13a2.1353 2.1262 0 0 1-2.1353 2.1262 2.1353 2.1262 0 0 1-2.1353-2.1262 2.1353 2.1262 0 0 1 2.1353-2.1262 2.1353 2.1262 0 0 1 2.1353 2.1262zm6.9586-2.6e-4a2.1353 2.1262 0 0 1-2.1353 2.1262 2.1353 2.1262 0 0 1-2.1353-2.1262 2.1353 2.1262 0 0 1 2.1353-2.1262 2.1353 2.1262 0 0 1 2.1353 2.1262zm-7.9068-3.2747 2.6988 3.2631h3.0844l-2.1205-3.2631h-3.6627m2.6988 3.2631 0.96386-4.0308m0 0 0.93952-2e-3m-5.7588 4.0328 1.6067-4.7631h1.5422" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width=".85781"/>
+<svg version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
+ <ellipse cx="63.992" cy="63.992" rx="63.992" ry="63.992" fill="#808080" stroke-width="3.7792"/>
+ <path d="m101.49 45.119c-2.7771 0-5.0297 2.2519-5.0297 5.0297 0 2.7776 2.2524 5.0285 5.0297 5.0285 2.7776 0 5.0297-2.251 5.0297-5.0285 0-2.7776-2.2519-5.0297-5.0297-5.0297zm-5.2614 11.888c-5.0886 1.9031-9.0125 5.137-9.0568 10.517 0 1.3772 1.1162 2.4925 2.4933 2.4925 1.3774 0 2.494-1.1153 2.4942-2.4925 0.04134-1.3749 0.62424-2.5421 1.4763-3.4747 0.02684 1.2249 0.22222 2.6 0.64363 4.1257-0.25952 0.43207-0.3679 1.0824-0.26568 2.0332-0.8799 5.0633-6.4624 8.358-10.061 9.9925-1.2588 0.55924-1.8263 2.0335-1.2672 3.2922 0.55966 1.2586 2.0335 1.8264 3.2922 1.2672 4.7757-2.4871 10.85-6.499 12.598-11.766 3.967 1.8494 7.6793 6.0043 8.3482 9.839 0.19444 1.3635 1.4584 2.3098 2.822 2.1154 1.3635-0.19482 2.3111-1.4567 2.1163-2.8202-1.0343-6.1544-5.2836-10.708-10.584-13.377-1.5353-0.65954-1.6568-4.2432-1.5703-5.5353 4.6503 2.64 9.4177 3.8188 14.136 1.5037 1.2321-0.6157 1.7305-2.1142 1.1147-3.3461-0.61612-1.2317-2.1124-1.7309-3.3442-1.1147-5.5455 2.7415-8.9989-1.4567-11.917-3.1561-1.0438-0.73119-2.4311-0.73338-3.4684-0.09493z" fill="#fff" stroke-width=".1264"/>
+ <rect x="59.994" y=".011353" width="7.9993" height="127.99" fill="#fff" stroke-width="3.7792"/>
+ <path d="m24.953 68.527a8.0696 8.0352 0 0 1-8.0696 8.0352 8.0696 8.0352 0 0 1-8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696 8.0352zm26.298-9.83e-4a8.0696 8.0352 0 0 1-8.0696 8.0352 8.0696 8.0352 0 0 1-8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696 8.0352zm-29.881-12.376 10.199 12.332h11.656l-8.0137-12.332h-13.842m10.199 12.332 3.6426-15.233m0 0 3.5506-0.0076m-21.763 15.241 6.072-18h5.8282" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.2418"/>
 </svg>
diff --git a/res/graphics/cycleway segregation/with_sidewalk.svg b/res/graphics/cycleway segregation/with_sidewalk.svg
new file mode 100644
index 00000000000..c3ce86d3eec
--- /dev/null
+++ b/res/graphics/cycleway segregation/with_sidewalk.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+  <clipPath id="clipPath12869">
+   <ellipse cx="64" cy="64" rx="64" ry="64" fill="#808080" stroke-width="3.7796"/>
+  </clipPath>
+ </defs>
+ <ellipse cx="64" cy="64" rx="64" ry="64" fill="#808080" stroke-width="3.7796"/>
+ <g clip-path="url(#clipPath12869)">
+  <path d="m76 0h52v128h-52z" fill="#aaa"/>
+  <path d="m76 88h16m-16-24h16m-16-48h16m-16 24h16m-16 72h16m16-24h16m-16-24h16m-16-48h16m-16 24h16m-16 72h16m-32 12h16m-16-48h16m-16-24h16m-16-48h16m-16 24h16m-16 72h16m16-100v128m-16 0v-128m-16 0v128m32-100.58h4m-4 24h4m-4 72h4m-4-24h4m-4-24h4m-4-72h4" fill="none" stroke="#999" stroke-width="4"/>
+  <path d="m68 0h8v128h-8z" fill="#999"/>
+  <path d="m60 0h8v128h-8z" fill="#666"/>
+ </g>
+ <path d="m101.49 45.119c-2.7771 0-5.0297 2.2519-5.0297 5.0297 0 2.7776 2.2524 5.0285 5.0297 5.0285 2.7776 0 5.0297-2.251 5.0297-5.0285 0-2.7776-2.2519-5.0297-5.0297-5.0297zm-5.2614 11.888c-5.0886 1.9031-9.0125 5.137-9.0568 10.517 0 1.3772 1.1162 2.4925 2.4933 2.4925 1.3774 0 2.494-1.1153 2.4942-2.4925 0.04134-1.3749 0.62424-2.5421 1.4763-3.4747 0.02684 1.2249 0.22222 2.6 0.64363 4.1257-0.25952 0.43207-0.3679 1.0824-0.26568 2.0332-0.8799 5.0633-6.4624 8.358-10.061 9.9925-1.2588 0.55924-1.8263 2.0335-1.2672 3.2922 0.55966 1.2586 2.0335 1.8264 3.2922 1.2672 4.7757-2.4871 10.85-6.499 12.598-11.766 3.967 1.8494 7.6793 6.0043 8.3482 9.839 0.19444 1.3635 1.4584 2.3098 2.822 2.1154 1.3635-0.19482 2.3111-1.4567 2.1163-2.8202-1.0343-6.1544-5.2836-10.708-10.584-13.377-1.5353-0.65954-1.6568-4.2432-1.5703-5.5353 4.6503 2.64 9.4177 3.8188 14.136 1.5037 1.2321-0.6157 1.7305-2.1142 1.1147-3.3461-0.61612-1.2317-2.1124-1.7309-3.3442-1.1147-5.5455 2.7415-8.9989-1.4567-11.917-3.1561-1.0438-0.73119-2.4311-0.73338-3.4684-0.09493z" fill="#fff" stroke-width=".1264"/>
+ <path d="m24.953 68.527a8.0696 8.0352 0 0 1-8.0696 8.0352 8.0696 8.0352 0 0 1-8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696 8.0352zm26.298-9.83e-4a8.0696 8.0352 0 0 1-8.0696 8.0352 8.0696 8.0352 0 0 1-8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696 8.0352zm-29.881-12.376 10.199 12.332h11.656l-8.0137-12.332h-13.842m10.199 12.332 3.6426-15.233m0 0 3.5506-0.0076m-21.763 15.241 6.072-18h5.8282" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.2418"/>
+</svg>
diff --git a/res/graphics/cycleway segregation/with_sidewalk_l.svg b/res/graphics/cycleway segregation/with_sidewalk_l.svg
new file mode 100644
index 00000000000..2827ad869e7
--- /dev/null
+++ b/res/graphics/cycleway segregation/with_sidewalk_l.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+  <clipPath id="clipPath12869">
+   <ellipse cx="64" cy="64" rx="64" ry="64" fill="#808080" stroke-width="3.7796"/>
+  </clipPath>
+ </defs>
+ <ellipse cx="64" cy="64" rx="64" ry="64" fill="#808080" stroke-width="3.7796"/>
+ <g transform="matrix(-1,0,0,1,128,0)" clip-path="url(#clipPath12869)">
+  <path d="m76 0h52v128h-52z" fill="#aaa"/>
+  <path d="m76 88h16m-16-24h16m-16-48h16m-16 24h16m-16 72h16m16-24h16m-16-24h16m-16-48h16m-16 24h16m-16 72h16m-32 12h16m-16-48h16m-16-24h16m-16-48h16m-16 24h16m-16 72h16m16-100v128m-16 0v-128m-16 0v128m32-100.58h4m-4 24h4m-4 72h4m-4-24h4m-4-24h4m-4-72h4" fill="none" stroke="#999" stroke-width="4"/>
+  <path d="m68 0h8v128h-8z" fill="#999"/>
+  <path d="m60 0h8v128h-8z" fill="#666"/>
+ </g>
+ <path d="m33.493 45.119c-2.7771 0-5.0297 2.2519-5.0297 5.0297 0 2.7776 2.2524 5.0285 5.0297 5.0285 2.7776 0 5.0297-2.251 5.0297-5.0285 0-2.7776-2.2519-5.0297-5.0297-5.0297zm-5.2614 11.888c-5.0886 1.9031-9.0125 5.137-9.0568 10.517 0 1.3772 1.1162 2.4925 2.4933 2.4925 1.3774 0 2.494-1.1153 2.4942-2.4925 0.04134-1.3749 0.62424-2.5421 1.4763-3.4747 0.02684 1.2249 0.22222 2.6 0.64363 4.1257-0.25952 0.43207-0.3679 1.0824-0.26568 2.0332-0.8799 5.0633-6.4624 8.358-10.061 9.9925-1.2588 0.55924-1.8263 2.0335-1.2672 3.2922 0.55966 1.2586 2.0335 1.8264 3.2922 1.2672 4.7757-2.4871 10.85-6.499 12.598-11.766 3.967 1.8494 7.6793 6.0043 8.3482 9.839 0.19444 1.3635 1.4584 2.3098 2.822 2.1154 1.3635-0.19482 2.3111-1.4567 2.1163-2.8202-1.0343-6.1544-5.2836-10.708-10.584-13.377-1.5353-0.65954-1.6568-4.2432-1.5703-5.5353 4.6503 2.64 9.4177 3.8188 14.136 1.5037 1.2321-0.6157 1.7305-2.1142 1.1147-3.3461-0.61612-1.2317-2.1124-1.7309-3.3442-1.1147-5.5455 2.7415-8.9989-1.4567-11.917-3.1561-1.0438-0.73119-2.4311-0.73338-3.4684-0.09493z" fill="#fff" stroke-width=".1264"/>
+ <path d="m92.953 68.527a8.0696 8.0352 0 0 1-8.0696 8.0352 8.0696 8.0352 0 0 1-8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696 8.0352zm26.298-9.83e-4a8.0696 8.0352 0 0 1-8.0696 8.0352 8.0696 8.0352 0 0 1-8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696-8.0352 8.0696 8.0352 0 0 1 8.0696 8.0352zm-29.881-12.376 10.199 12.332h11.656l-8.0137-12.332h-13.842m10.199 12.332 3.6426-15.233m0 0 3.5506-0.0076m-21.763 15.241 6.072-18h5.8282" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.2418"/>
+</svg>
diff --git a/res/graphics/cycleway/bicycle.svg b/res/graphics/cycleway/bicycle.svg
new file mode 100644
index 00000000000..df18c0f55ce
--- /dev/null
+++ b/res/graphics/cycleway/bicycle.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 128 85" xmlns="http://www.w3.org/2000/svg">
+ <path d="m50.355 55.687a21.678 21.678 0 0 1-21.678 21.678 21.678 21.678 0 0 1-21.678-21.678 21.678 21.678 0 0 1 21.678-21.678 21.678 21.678 0 0 1 21.678 21.678m70.645-4e-3a21.678 21.678 0 0 1-21.678 21.678 21.678 21.678 0 0 1-21.678-21.678 21.678 21.678 0 0 1 21.678-21.678 21.678 21.678 0 0 1 21.678 21.678m-80.27-33.389 27.398 33.269h31.313l-21.527-33.269h-37.183m37.183-7.8281 9.5379-0.01958m-19.323 41.116 9.785-41.098m-48.927 41.098 16.311-48.562h15.656" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.0002"/>
+</svg>
diff --git a/res/graphics/cycleway/shoulder.svg b/res/graphics/cycleway/shoulder.svg
new file mode 100644
index 00000000000..b0de9a09edf
--- /dev/null
+++ b/res/graphics/cycleway/shoulder.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
+ <rect y="6.1377e-5" width="208" height="256" fill="#808080"/>
+ <rect x="112" y="3.2244e-6" width="16" height="256" fill="#fff"/>
+</svg>


Test patch:
diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/osmquests/TestQuestType.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/osmquests/TestQuestType.kt
index 176fed3129c..7b97c648cbe 100644
--- a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/osmquests/TestQuestType.kt
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/osmquests/TestQuestType.kt
@@ -1,5 +1,6 @@
 package de.westnordost.streetcomplete.data.osm.osmquests
 
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement
@@ -9,7 +10,7 @@ import de.westnordost.streetcomplete.quests.AbstractOsmQuestForm
 open class TestQuestType : OsmElementQuestType<String> {
 
     override fun isApplicableTo(element: Element): Boolean? = null
-    override fun applyAnswerTo(answer: String, tags: Tags, timestampEdited: Long) {}
+    override fun applyAnswerTo(answer: String, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {}
     override val icon = 0
     override fun createForm(): AbstractOsmQuestForm<String> = object : AbstractOsmQuestForm<String>() {}
     override val changesetComment = ""
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/quest/TestQuestTypes.kt b/app/src/test/java/de/westnordost/streetcomplete/data/quest/TestQuestTypes.kt
index ad0cc0a8f0b..246bc53c6f0 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/data/quest/TestQuestTypes.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/quest/TestQuestTypes.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.data.quest
 
 import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
@@ -12,7 +13,7 @@ open class TestQuestTypeA : OsmElementQuestType<String> {
 
     override fun getTitle(tags: Map<String, String>) = 0
     override fun isApplicableTo(element: Element): Boolean? = null
-    override fun applyAnswerTo(answer: String, tags: Tags, timestampEdited: Long) {}
+    override fun applyAnswerTo(answer: String, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {}
     override fun createForm(): AbstractOsmQuestForm<String> = object : AbstractOsmQuestForm<String>() {}
     override val changesetComment = "test me"
     override fun getApplicableElements(mapData: MapDataWithGeometry) = mapData.filter { isApplicableTo(it) == true }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/OnewayKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/OnewayKtTest.kt
index 359090a8b2b..dcf7e4b2e8e 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/osm/OnewayKtTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/OnewayKtTest.kt
@@ -37,4 +37,31 @@ class OnewayKtTest {
             assertTrue(isNotOnewayForCyclists(mapOf("oneway:bicycle" to "no"), isLeftHandTraffic))
         }
     }
+
+    @Test fun `is in contraflow of oneway`() {
+        val oneway = mapOf("oneway" to "yes")
+        val reverseOneway = mapOf("oneway" to "-1")
+
+        // not in oneway
+        assertFalse(isInContraflowOfOneway(false, mapOf(), false))
+        assertFalse(isInContraflowOfOneway(false, mapOf(), true))
+        assertFalse(isInContraflowOfOneway(true, mapOf(), false))
+        assertFalse(isInContraflowOfOneway(true, mapOf(), true))
+
+        // sides of oneway
+        assertTrue(isInContraflowOfOneway(false, oneway, false))
+        assertFalse(isInContraflowOfOneway(true, oneway, false))
+
+        // sides of reverse oneway
+        assertFalse(isInContraflowOfOneway(false, reverseOneway, false))
+        assertTrue(isInContraflowOfOneway(true, reverseOneway, false))
+
+        // sides of reverse oneway in left hand traffic countries
+        assertTrue(isInContraflowOfOneway(false, reverseOneway, true))
+        assertFalse(isInContraflowOfOneway(true, reverseOneway, true))
+
+        // sides of oneway in left hand traffic countries
+        assertFalse(isInContraflowOfOneway(false, oneway, true))
+        assertTrue(isInContraflowOfOneway(true, oneway, true))
+    }
 }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/TagsKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/TagsKtTest.kt
new file mode 100644
index 00000000000..0f53a80f6f7
--- /dev/null
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/TagsKtTest.kt
@@ -0,0 +1,102 @@
+package de.westnordost.streetcomplete.osm
+
+import org.junit.Assert.*
+import org.junit.Test
+
+class TagsKtTest {
+
+    @Test fun `expand sides`() {
+        val tags = Tags(mapOf("a:both" to "blub", "a" to "norg"))
+        tags.expandSides("a", includeBareTag = false)
+        assertEquals(
+            mapOf("a:left" to "blub", "a:right" to "blub", "a" to "norg"),
+            tags.toMap()
+        )
+    }
+
+    @Test fun `expand sides with bare tag`() {
+        val tags = Tags(mapOf("a" to "blub"))
+        tags.expandSides("a")
+        assertEquals(
+            mapOf("a:left" to "blub", "a:right" to "blub"),
+            tags.toMap()
+        )
+    }
+
+    @Test fun `when expanding sides, explicit tag takes precedence over bare tag`() {
+        val tags = Tags(mapOf("a" to "blub", "a:both" to "burg"))
+        tags.expandSides("a")
+        assertEquals(
+            mapOf("a:left" to "burg", "a:right" to "burg"),
+            tags.toMap()
+        )
+    }
+
+    @Test fun `when expanding sides, does not overwrite left and right tags`() {
+        val tags = Tags(mapOf("a:both" to "gah", "a:left" to "le", "a:right" to "ri"))
+        tags.expandSides("a")
+        assertEquals(
+            mapOf("a:left" to "le", "a:right" to "ri"),
+            tags.toMap()
+        )
+    }
+
+    @Test fun `don't expand unrelated tags sides`() {
+        val tags = Tags(mapOf("abc:both" to "blub"))
+        tags.expandSides("a", includeBareTag = false)
+        assertEquals(
+            mapOf("abc:both" to "blub"),
+            tags.toMap()
+        )
+    }
+
+    @Test fun `expand sides with postfix`() {
+        val tags = Tags(mapOf("a:both:c" to "blub", "a:c" to "blarg"))
+        tags.expandSides("a", "c", includeBareTag = false)
+        assertEquals(
+            mapOf("a:left:c" to "blub", "a:right:c" to "blub", "a:c" to "blarg"),
+            tags.toMap()
+        )
+
+        val tags2 = Tags(mapOf("a:c" to "blarg"))
+        tags2.expandSides("a", "c")
+        assertEquals(
+            mapOf("a:left:c" to "blarg", "a:right:c" to "blarg"),
+            tags2.toMap()
+        )
+    }
+
+    @Test fun `merge sides`() {
+        val tags = Tags(mapOf("a:left" to "blub", "a:right" to "blub"))
+        tags.mergeSides("a")
+        assertEquals(
+            mapOf("a:both" to "blub"),
+            tags.toMap()
+        )
+    }
+
+    @Test fun `merge sides only merges if both are the same`() {
+        val tags = Tags(mapOf("a:left" to "blub"))
+        tags.mergeSides("a")
+        assertEquals(
+            mapOf("a:left" to "blub"),
+            tags.toMap()
+        )
+
+        val tags2 = Tags(mapOf("a:left" to "blub", "a:right" to "blab"))
+        tags2.mergeSides("a")
+        assertEquals(
+            mapOf("a:left" to "blub", "a:right" to "blab"),
+            tags2.toMap()
+        )
+    }
+
+    @Test fun `merge sides overwrites previous both tag`() {
+        val tags = Tags(mapOf("a:left" to "blub", "a:right" to "blub", "a:both" to "old"))
+        tags.mergeSides("a")
+        assertEquals(
+            mapOf("a:both" to "blub"),
+            tags.toMap()
+        )
+    }
+}
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/bicycle_boulevard/BicycleBoulevardKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/bicycle_boulevard/BicycleBoulevardKtTest.kt
new file mode 100644
index 00000000000..862846d3888
--- /dev/null
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/bicycle_boulevard/BicycleBoulevardKtTest.kt
@@ -0,0 +1,88 @@
+package de.westnordost.streetcomplete.osm.bicycle_boulevard
+
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
+import de.westnordost.streetcomplete.osm.bicycle_boulevard.BicycleBoulevard.*
+import org.assertj.core.api.Assertions
+import org.junit.Assert.*
+import org.junit.Test
+
+class BicycleBoulevardKtTest {
+
+    @Test fun create() {
+        assertEquals(NO, createBicycleBoulevard(mapOf()))
+        assertEquals(NO, createBicycleBoulevard(mapOf("bicycle_road" to "no")))
+        assertEquals(YES, createBicycleBoulevard(mapOf("bicycle_road" to "yes")))
+        assertEquals(NO, createBicycleBoulevard(mapOf("cyclestreet" to "no")))
+        assertEquals(YES, createBicycleBoulevard(mapOf("cyclestreet" to "yes")))
+    }
+
+    @Test fun `apply yes when it was not tagged before`() {
+        verifyAnswer(mapOf(), YES, "DE", arrayOf(StringMapEntryAdd("bicycle_road", "yes")))
+        verifyAnswer(mapOf(), YES, "US", arrayOf(StringMapEntryAdd("bicycle_road", "yes")))
+
+        verifyAnswer(mapOf(), YES, "BE", arrayOf(StringMapEntryAdd("cyclestreet", "yes")))
+        verifyAnswer(mapOf(), YES, "NL", arrayOf(StringMapEntryAdd("cyclestreet", "yes")))
+        verifyAnswer(mapOf(), YES, "LU", arrayOf(StringMapEntryAdd("cyclestreet", "yes")))
+    }
+
+    @Test fun `apply yes when it was tagged before`() {
+        // modifying current tag
+        verifyAnswer(
+            mapOf("bicycle_road" to "no"),
+            YES, "DE",
+            arrayOf(StringMapEntryModify("bicycle_road", "no", "yes"))
+        )
+        verifyAnswer(
+            mapOf("cyclestreet" to "no"),
+            YES, "NL",
+            arrayOf(StringMapEntryModify("cyclestreet", "no", "yes"))
+        )
+
+        // keeping current tag in country where that tag is not usually used
+        verifyAnswer(
+            mapOf("cyclestreet" to "no"),
+            YES, "DE",
+            arrayOf(StringMapEntryModify("cyclestreet", "no", "yes"))
+        )
+        verifyAnswer(
+            mapOf("bicycle_road" to "no"),
+            YES, "NL",
+            arrayOf(StringMapEntryModify("bicycle_road", "no", "yes"))
+        )
+    }
+
+    @Test fun `apply no`() {
+        verifyAnswer(
+            mapOf("bicycle_road" to "yes", "cyclestreet" to "yes"),
+            NO, "DE",
+            arrayOf(
+                StringMapEntryDelete("bicycle_road", "yes"),
+                StringMapEntryDelete("cyclestreet", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf("bicycle_road" to "yes", "cyclestreet" to "yes"),
+            NO, "DE",
+            arrayOf(
+                StringMapEntryDelete("bicycle_road", "yes"),
+                StringMapEntryDelete("cyclestreet", "yes"),
+            )
+        )
+    }
+}
+
+private fun verifyAnswer(
+    tags: Map<String, String>,
+    answer: BicycleBoulevard,
+    countryCode: String,
+    expectedChanges: Array<StringMapEntryChange>
+) {
+    val cb = StringMapChangesBuilder(tags)
+    answer.applyTo(cb, countryCode)
+    val changes = cb.create().changes
+    Assertions.assertThat(changes).containsExactlyInAnyOrder(*expectedChanges)
+}
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreatorKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreatorKtTest.kt
new file mode 100644
index 00000000000..95f1d887052
--- /dev/null
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreatorKtTest.kt
@@ -0,0 +1,728 @@
+package de.westnordost.streetcomplete.osm.cycleway
+
+import de.westnordost.streetcomplete.osm.cycleway.Cycleway.*
+import de.westnordost.streetcomplete.osm.cycleway.Direction.*
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
+import org.assertj.core.api.Assertions
+import org.junit.Test
+
+class CyclewayCreatorKtTest {
+
+    @Test fun `apply nothing`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:left" to "track",
+                "cycleway" to "no",
+                "cycleway:right" to "track"
+            ),
+            LeftAndRightCycleway(null, null),
+            arrayOf()
+        )
+    }
+
+    @Test fun `apply cycleway lane answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "lane"),
+                StringMapEntryAdd("cycleway:both:lane", "exclusive")
+            )
+        )
+    }
+
+    @Test fun `apply advisory lane answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(ADVISORY_LANE, ADVISORY_LANE),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "lane"),
+                StringMapEntryAdd("cycleway:both:lane", "advisory")
+            )
+        )
+    }
+
+    @Test fun `apply cycleway track answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(TRACK, TRACK),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "track")
+            )
+        )
+    }
+
+    @Test fun `apply unspecified cycle lane answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(UNSPECIFIED_LANE, UNSPECIFIED_LANE),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "lane")
+            )
+        )
+    }
+
+    @Test fun `apply unspecified cycle lane answer does not remove previous specific lane answer`() {
+        verifyAnswer(
+            mapOf("cycleway:both:lane" to "exclusive"),
+            cycleway(UNSPECIFIED_LANE, UNSPECIFIED_LANE),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "lane"),
+                StringMapEntryModify("cycleway:both:lane", "exclusive", "exclusive"),
+            )
+        )
+    }
+
+    @Test fun `apply bus lane answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(BUSWAY, BUSWAY),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "share_busway")
+            )
+        )
+    }
+
+    @Test fun `apply pictogram lane answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(PICTOGRAMS, PICTOGRAMS),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "shared_lane"),
+                StringMapEntryAdd("cycleway:both:lane", "pictogram")
+            )
+        )
+    }
+
+    @Test fun `apply suggestion lane answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(SUGGESTION_LANE, SUGGESTION_LANE),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "shared_lane"),
+                StringMapEntryAdd("cycleway:both:lane", "advisory")
+            )
+        )
+    }
+
+    @Test fun `apply no cycleway answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(NONE, NONE),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "no")
+            )
+        )
+    }
+
+    @Test fun `apply cycleway on sidewalk answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "track"),
+                StringMapEntryAdd("cycleway:both:segregated", "no"),
+                StringMapEntryAdd("sidewalk", "both"),
+            )
+        )
+    }
+
+    @Test fun `apply cycleway on sidewalk answer on one side only`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(null, SIDEWALK_EXPLICIT),
+            arrayOf(
+                StringMapEntryAdd("cycleway:right", "track"),
+                StringMapEntryAdd("cycleway:right:segregated", "no"),
+                StringMapEntryAdd("sidewalk:right", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf("sidewalk" to "right"),
+            cycleway(SIDEWALK_EXPLICIT, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "track"),
+                StringMapEntryAdd("cycleway:left:segregated", "no"),
+                StringMapEntryModify("sidewalk", "right", "both"),
+            )
+        )
+    }
+
+    @Test fun `apply separate cycleway answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(SEPARATE, SEPARATE),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "separate")
+            )
+        )
+    }
+
+    @Test fun `apply dual cycle track answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(TRACK to BOTH, TRACK to BOTH),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "track"),
+                StringMapEntryAdd("cycleway:both:oneway", "no")
+            )
+        )
+    }
+
+    @Test fun `apply dual cycle track answer in oneway`() {
+        verifyAnswer(
+            mapOf("oneway" to "yes"),
+            cycleway(TRACK to BOTH, TRACK to BOTH),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "track"),
+                StringMapEntryAdd("cycleway:both:oneway", "no"),
+                StringMapEntryAdd("oneway:bicycle", "no"),
+            )
+        )
+    }
+
+    @Test fun `apply dual cycle lane answer`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(EXCLUSIVE_LANE to BOTH, EXCLUSIVE_LANE to BOTH),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "lane"),
+                StringMapEntryAdd("cycleway:both:oneway", "no"),
+                StringMapEntryAdd("cycleway:both:lane", "exclusive")
+            )
+        )
+    }
+
+    @Test fun `apply dual cycle lane answer in oneway`() {
+        verifyAnswer(
+            mapOf("oneway" to "yes"),
+            cycleway(EXCLUSIVE_LANE to BOTH, EXCLUSIVE_LANE to BOTH),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "lane"),
+                StringMapEntryAdd("cycleway:both:oneway", "no"),
+                StringMapEntryAdd("cycleway:both:lane", "exclusive"),
+                StringMapEntryAdd("oneway:bicycle", "no"),
+            )
+        )
+    }
+
+    @Test fun `apply answer where left and right side are different`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(TRACK, NONE),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "track"),
+                StringMapEntryAdd("cycleway:right", "no")
+            )
+        )
+    }
+
+    @Test fun `apply answer where there exists a cycleway in opposite direction of oneway`() {
+        verifyAnswer(
+            mapOf("oneway" to "yes"),
+            cycleway(TRACK, TRACK),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "track"),
+                StringMapEntryAdd("cycleway:left:oneway", "-1"),
+                StringMapEntryAdd("oneway:bicycle", "no")
+            )
+        )
+    }
+
+    @Test fun `apply answer where there exists a cycleway in opposite direction of backward oneway`() {
+        verifyAnswer(
+            mapOf("oneway" to "-1"),
+            cycleway(TRACK, TRACK),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "track"),
+                StringMapEntryAdd("cycleway:right:oneway", "yes"),
+                StringMapEntryAdd("oneway:bicycle", "no")
+            )
+        )
+    }
+
+    @Test fun `apply cycleway track answer updates segregated key`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:both:segregated" to "no",
+                "cycleway:both" to "no"
+            ),
+            cycleway(TRACK, TRACK),
+            arrayOf(
+                StringMapEntryModify("cycleway:both:segregated", "no", "yes"),
+                StringMapEntryModify("cycleway:both", "no", "track")
+            )
+        )
+    }
+
+    @Test fun `apply answer for both deletes any previous answers given for left, right, general`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:left" to "lane",
+                "cycleway:left:lane" to "advisory",
+                "cycleway:left:segregated" to "maybe",
+                "cycleway:left:oneway" to "yes",
+                "cycleway:right" to "shared_lane",
+                "cycleway:right:lane" to "pictogram",
+                "cycleway:right:segregated" to "definitely",
+                "cycleway:right:oneway" to "yes",
+                "cycleway" to "shared_lane",
+                "cycleway:lane" to "pictogram",
+                "cycleway:segregated" to "definitely",
+                "cycleway:oneway" to "yes"
+            ),
+            cycleway(TRACK, TRACK),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "track"),
+                StringMapEntryDelete("cycleway:left", "lane"),
+                StringMapEntryDelete("cycleway:left:lane", "advisory"),
+                StringMapEntryDelete("cycleway:left:segregated", "maybe"),
+                StringMapEntryDelete("cycleway:right", "shared_lane"),
+                StringMapEntryDelete("cycleway:right:lane", "pictogram"),
+                StringMapEntryDelete("cycleway:right:segregated", "definitely"),
+                StringMapEntryDelete("cycleway", "shared_lane"),
+                StringMapEntryDelete("cycleway:lane", "pictogram"),
+                StringMapEntryDelete("cycleway:segregated", "definitely"),
+                StringMapEntryDelete("cycleway:oneway", "yes"),
+                StringMapEntryModify("cycleway:left:oneway", "yes", "-1"),
+                StringMapEntryModify("cycleway:right:oneway", "yes", "yes"),
+                StringMapEntryAdd("cycleway:both:segregated", "yes"),
+            )
+        )
+    }
+
+    @Test fun `apply answer for left, right deletes any previous answers given for both, general`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:both" to "shared_lane",
+                "cycleway:both:lane" to "pictogram",
+                "cycleway" to "shared_lane",
+                "cycleway:lane" to "pictogram",
+                "cycleway:segregated" to "yes",
+                "cycleway:oneway" to "yes",
+            ),
+            cycleway(TRACK, NONE),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "track"),
+                StringMapEntryAdd("cycleway:right", "no"),
+                StringMapEntryDelete("cycleway:both", "shared_lane"),
+                StringMapEntryDelete("cycleway:both:lane", "pictogram"),
+                StringMapEntryDelete("cycleway", "shared_lane"),
+                StringMapEntryDelete("cycleway:lane", "pictogram"),
+                StringMapEntryDelete("cycleway:oneway", "yes"),
+                StringMapEntryDelete("cycleway:segregated", "yes"),
+                StringMapEntryAdd("cycleway:left:oneway", "-1"),
+                StringMapEntryAdd("cycleway:left:segregated", "yes"),
+            )
+        )
+    }
+
+    @Test fun `deletes lane subkey when new answer is not a lane`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:both" to "lane",
+                "cycleway:both:lane" to "exclusive"
+            ),
+            cycleway(TRACK, TRACK),
+            arrayOf(
+                StringMapEntryModify("cycleway:both", "lane", "track"),
+                StringMapEntryDelete("cycleway:both:lane", "exclusive")
+            )
+        )
+    }
+
+    @Test fun `deletes shared lane subkey when new answer is not a lane`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:both" to "shared_lane",
+                "cycleway:both:lane" to "pictogram"
+            ),
+            cycleway(TRACK, TRACK),
+            arrayOf(
+                StringMapEntryModify("cycleway:both", "shared_lane", "track"),
+                StringMapEntryDelete("cycleway:both:lane", "pictogram")
+            )
+        )
+    }
+
+    @Test fun `modifies oneway tag tag when new answer is not a dual lane`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:both" to "lane",
+                "cycleway:both:lane" to "exclusive",
+                "cycleway:both:oneway" to "no"
+            ),
+            cycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
+            arrayOf(
+                StringMapEntryModify("cycleway:both", "lane", "lane"),
+                StringMapEntryModify("cycleway:both:lane", "exclusive", "exclusive"),
+                StringMapEntryDelete("cycleway:both:oneway", "no"),
+                StringMapEntryAdd("cycleway:left:oneway", "-1"),
+                StringMapEntryAdd("cycleway:right:oneway", "yes"),
+            )
+        )
+    }
+
+    @Test fun `modifies lane subkey when new answer is different lane`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:both" to "shared_lane",
+                "cycleway:both:lane" to "pictogram"
+            ),
+            cycleway(SUGGESTION_LANE, SUGGESTION_LANE),
+            arrayOf(
+                StringMapEntryModify("cycleway:both", "shared_lane", "shared_lane"),
+                StringMapEntryModify("cycleway:both:lane", "pictogram", "advisory")
+            )
+        )
+    }
+
+    @Test fun `modifies oneway tag when new answer is not a dual track`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:both" to "track",
+                "cycleway:both:oneway" to "no"
+            ),
+            cycleway(TRACK, TRACK),
+            arrayOf(
+                StringMapEntryModify("cycleway:both", "track", "track"),
+                StringMapEntryDelete("cycleway:both:oneway", "no"),
+                StringMapEntryAdd("cycleway:left:oneway", "-1"),
+                StringMapEntryAdd("cycleway:right:oneway", "yes"),
+            )
+        )
+    }
+
+    @Test fun `modify segregated tag if new answer is now segregated`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:both" to "track",
+                "cycleway:both:segregated" to "no"
+            ),
+            cycleway(TRACK, TRACK),
+            arrayOf(
+                StringMapEntryModify("cycleway:both", "track", "track"),
+                StringMapEntryModify("cycleway:both:segregated", "no", "yes")
+            )
+        )
+    }
+
+    @Test fun `modify segregated tag if new answer is now not segregated`() {
+        verifyAnswer(
+            mapOf(
+                "sidewalk" to "both",
+                "cycleway:both" to "track",
+                "cycleway:both:segregated" to "yes"
+            ),
+            cycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
+            arrayOf(
+                StringMapEntryModify("sidewalk", "both", "both"),
+                StringMapEntryModify("cycleway:both", "track", "track"),
+                StringMapEntryModify("cycleway:both:segregated", "yes", "no")
+            )
+        )
+    }
+
+    @Test fun `delete segregated tag if new answer is not a track or on sidewalk`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:both" to "track",
+                "cycleway:both:segregated" to "no"
+            ),
+            cycleway(BUSWAY, BUSWAY),
+            arrayOf(
+                StringMapEntryModify("cycleway:both", "track", "share_busway"),
+                StringMapEntryDelete("cycleway:both:segregated", "no")
+            )
+        )
+    }
+
+    @Test fun `sets check date if nothing changed`() {
+        verifyAnswer(
+            mapOf("cycleway:both" to "track"),
+            cycleway(TRACK, TRACK),
+            arrayOf(
+                StringMapEntryModify("cycleway:both", "track", "track"),
+                StringMapEntryAdd("check_date:cycleway", nowAsCheckDateString())
+            )
+        )
+    }
+
+    @Test fun `updates check date if nothing changed`() {
+        verifyAnswer(
+            mapOf("cycleway:both" to "track", "check_date:cycleway" to "2000-11-11"),
+            cycleway(TRACK, TRACK),
+            arrayOf(
+                StringMapEntryModify("cycleway:both", "track", "track"),
+                StringMapEntryModify("check_date:cycleway", "2000-11-11", nowAsCheckDateString())
+            )
+        )
+    }
+
+    @Test fun `remove oneway bicycle no tag if road is also a oneway for bicycles now`() {
+        verifyAnswer(
+            mapOf(
+                "cycleway:both" to "no",
+                "oneway" to "yes",
+                "oneway:bicycle" to "no"
+            ),
+            cycleway(NONE, NONE),
+            arrayOf(
+                StringMapEntryModify("cycleway:both", "no", "no"),
+                StringMapEntryDelete("oneway:bicycle", "no")
+            )
+        )
+    }
+
+    @Test fun `apply value only for one side`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(TRACK, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "track")
+            )
+        )
+        verifyAnswer(
+            mapOf(),
+            cycleway(null, TRACK),
+            arrayOf(
+                StringMapEntryAdd("cycleway:right", "track")
+            )
+        )
+    }
+
+    @Test fun `apply for one side does not touch the other side`() {
+        verifyAnswer(
+            mapOf("cycleway:right" to "no"),
+            cycleway(TRACK, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "track")
+            )
+        )
+        verifyAnswer(
+            mapOf("cycleway:left" to "no"),
+            cycleway(null, TRACK),
+            arrayOf(
+                StringMapEntryAdd("cycleway:right", "track")
+            )
+        )
+    }
+
+    @Test fun `apply for one side does not touch the other side even if it is invalid`() {
+        verifyAnswer(
+            mapOf("cycleway:right" to "invalid"),
+            cycleway(TRACK, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "track")
+            )
+        )
+        verifyAnswer(
+            mapOf("cycleway:left" to "invalid"),
+            cycleway(null, TRACK),
+            arrayOf(
+                StringMapEntryAdd("cycleway:right", "track")
+            )
+        )
+    }
+
+    @Test fun `apply for one side does not change values for the other side even if it was defined for both sides before and invalid`() {
+        verifyAnswer(
+            mapOf("cycleway:both" to "invalid"),
+            cycleway(TRACK, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "track"),
+                StringMapEntryAdd("cycleway:right", "invalid"),
+                StringMapEntryDelete("cycleway:both", "invalid"),
+            )
+        )
+        verifyAnswer(
+            mapOf("cycleway:both" to "invalid"),
+            cycleway(null, TRACK),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "invalid"),
+                StringMapEntryAdd("cycleway:right", "track"),
+                StringMapEntryDelete("cycleway:both", "invalid"),
+            )
+        )
+    }
+
+    @Test fun `apply conflates values`() {
+        verifyAnswer(
+            mapOf("cycleway:left" to "no", "cycleway:right" to "no"),
+            cycleway(NONE, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "no"),
+                StringMapEntryDelete("cycleway:left", "no"),
+                StringMapEntryDelete("cycleway:right", "no"),
+            )
+        )
+    }
+
+    @Test fun `apply updates oneway not for cyclists`() {
+        // contra-flow side set to explicitly not oneway -> not oneway for cyclists
+        verifyAnswer(
+            mapOf(
+                "oneway:bicycle" to "no",
+                "oneway" to "yes"
+            ),
+            cycleway(NONE_NO_ONEWAY, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "no"),
+                StringMapEntryModify("oneway:bicycle", "no", "no"),
+            )
+        )
+        // track on right side is dual-way -> not oneway for cyclists
+        verifyAnswer(
+            mapOf(
+                "oneway:bicycle" to "no",
+                "oneway" to "yes"
+            ),
+            cycleway(null, TRACK to BOTH),
+            arrayOf(
+                StringMapEntryAdd("cycleway:right", "track"),
+                StringMapEntryAdd("cycleway:right:oneway", "no"),
+                StringMapEntryModify("oneway:bicycle", "no", "no"),
+            )
+        )
+        // not a oneway at all
+        verifyAnswer(
+            mapOf(
+                "oneway:bicycle" to "no",
+                "oneway" to "no"
+            ),
+            cycleway(NONE, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "no"),
+                StringMapEntryDelete("oneway:bicycle", "no"),
+            )
+        )
+        // contra-flow side is explicitly oneway and flow-side is not dual-way
+        verifyAnswer(
+            mapOf(
+                "oneway:bicycle" to "no",
+                "oneway" to "yes"
+            ),
+            cycleway(NONE, NONE),
+            arrayOf(
+                StringMapEntryAdd("cycleway:both", "no"),
+                StringMapEntryDelete("oneway:bicycle", "no"),
+            )
+        )
+        // no oneway for cyclists is deleted on left side but dual track on right side still there
+        // -> still not oneway for cyclists
+        verifyAnswer(
+            mapOf(
+                "oneway:bicycle" to "no",
+                "oneway" to "yes",
+                "cycleway:both" to "track",
+                "cycleway:right:oneway" to "no",
+            ),
+            cycleway(NONE, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "no"),
+                StringMapEntryAdd("cycleway:right", "track"),
+                StringMapEntryDelete("cycleway:both", "track"),
+                StringMapEntryModify("oneway:bicycle", "no", "no"),
+            )
+        )
+        // no oneway for cyclists is deleted on left side -> oneway for cyclists now
+        verifyAnswer(
+            mapOf(
+                "oneway:bicycle" to "no",
+                "oneway" to "yes",
+                "cycleway:left" to "no",
+                "cycleway:right" to "track",
+            ),
+            cycleway(NONE, null),
+            arrayOf(
+                StringMapEntryModify("cycleway:left", "no", "no"),
+                StringMapEntryDelete("oneway:bicycle", "no"),
+            )
+        )
+    }
+
+    @Test fun `apply no cycleway deletes cycleway direction`() {
+        verifyAnswer(
+            mapOf("cycleway:left:oneway" to "-1"),
+            cycleway(NONE, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "no"),
+                StringMapEntryDelete("cycleway:left:oneway", "-1"),
+            )
+        )
+        verifyAnswer(
+            mapOf("cycleway:left:oneway" to "-1", "oneway" to "yes"),
+            cycleway(NONE_NO_ONEWAY, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "no"),
+                StringMapEntryDelete("cycleway:left:oneway", "-1"),
+                StringMapEntryAdd("oneway:bicycle", "no"),
+            )
+        )
+        verifyAnswer(
+            mapOf("cycleway:left:oneway" to "-1"),
+            cycleway(SEPARATE, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "separate"),
+                StringMapEntryDelete("cycleway:left:oneway", "-1"),
+            )
+        )
+    }
+
+    @Test fun `apply cycleway with non-standard direction adds cycleway direction`() {
+        verifyAnswer(
+            mapOf(),
+            cycleway(TRACK to FORWARD, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "track"),
+                StringMapEntryAdd("cycleway:left:oneway", "yes"),
+            )
+        )
+    }
+
+    @Test fun `apply cycleway in contraflow of oneway adds cycleway direction`() {
+        verifyAnswer(
+            mapOf("oneway" to "yes"),
+            cycleway(TRACK, null),
+            arrayOf(
+                StringMapEntryAdd("cycleway:left", "track"),
+                StringMapEntryAdd("cycleway:left:oneway", "-1"),
+                StringMapEntryAdd("oneway:bicycle", "no"),
+            )
+        )
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun `applying invalid left throws exception`() {
+        cycleway(INVALID, null).applyTo(StringMapChangesBuilder(mapOf()), false)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun `applying invalid right throws exception`() {
+        cycleway(null, INVALID).applyTo(StringMapChangesBuilder(mapOf()), false)
+    }
+}
+
+private fun verifyAnswer(tags: Map<String, String>, answer: LeftAndRightCycleway, expectedChanges: Array<StringMapEntryChange>) {
+    val cb = StringMapChangesBuilder(tags)
+    answer.applyTo(cb, false)
+    val changes = cb.create().changes
+    Assertions.assertThat(changes).containsExactlyInAnyOrder(*expectedChanges)
+}
+
+private fun cycleway(left: Pair<Cycleway, Direction>?, right: Pair<Cycleway, Direction>?) =
+    LeftAndRightCycleway(
+        left?.let { CyclewayAndDirection(it.first, it.second) },
+        right?.let { CyclewayAndDirection(it.first, it.second) },
+    )
+
+private fun cycleway(left: Cycleway?, right: Cycleway?, isLeftHandTraffic: Boolean = false) =
+    LeftAndRightCycleway(
+        left?.let { CyclewayAndDirection(it, if (isLeftHandTraffic) FORWARD else BACKWARD) },
+        right?.let { CyclewayAndDirection(it, if (isLeftHandTraffic) BACKWARD else FORWARD) },
+    )
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayKtTest.kt
new file mode 100644
index 00000000000..5095a5c322a
--- /dev/null
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayKtTest.kt
@@ -0,0 +1,144 @@
+package de.westnordost.streetcomplete.osm.cycleway
+
+import de.westnordost.streetcomplete.osm.cycleway.Cycleway.*
+import de.westnordost.streetcomplete.osm.cycleway.Direction.*
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class CyclewayKtTest {
+
+    @Test fun `was no oneway for cyclists but now it is`() {
+        assertTrue(
+            LeftAndRightCycleway(
+                CyclewayAndDirection(NONE, BACKWARD),
+                CyclewayAndDirection(NONE, FORWARD)
+            )
+            .wasNoOnewayForCyclistsButNowItIs(mapOf(
+                "oneway" to "yes",
+                "oneway:bicycle" to "no",
+                "cycleway:both" to "no"
+            ), false)
+        )
+    }
+
+    @Test fun `was no oneway for cyclists and still is`() {
+        assertFalse(
+            LeftAndRightCycleway(
+                CyclewayAndDirection(NONE_NO_ONEWAY, BACKWARD),
+                CyclewayAndDirection(NONE, FORWARD)
+            )
+            .wasNoOnewayForCyclistsButNowItIs(mapOf(
+                "oneway" to "yes",
+                "oneway:bicycle" to "no",
+                "cycleway:both" to "no"
+            ), false)
+        )
+    }
+
+    @Test fun `was oneway for cyclists and still is`() {
+        assertFalse(
+            LeftAndRightCycleway(
+                CyclewayAndDirection(NONE, BACKWARD),
+                CyclewayAndDirection(NONE, FORWARD)
+            )
+            .wasNoOnewayForCyclistsButNowItIs(mapOf(
+                "oneway" to "yes",
+                "cycleway:both" to "no"
+            ), false)
+        )
+    }
+
+    @Test fun `was oneway for cyclists and is not anymore`() {
+        assertFalse(
+            LeftAndRightCycleway(
+                CyclewayAndDirection(NONE_NO_ONEWAY, BACKWARD),
+                CyclewayAndDirection(NONE, FORWARD)
+            )
+            .wasNoOnewayForCyclistsButNowItIs(mapOf(
+                "oneway" to "yes",
+                "cycleway:both" to "no"
+            ), false)
+        )
+    }
+
+    @Test fun `not a oneway is no oneway for cyclists`() {
+        assertTrue(
+            LeftAndRightCycleway(
+                CyclewayAndDirection(NONE, BACKWARD),
+                CyclewayAndDirection(NONE, FORWARD)
+            ).isNotOnewayForCyclistsNow(mapOf(), false)
+        )
+    }
+
+    @Test fun `forward oneway is oneway for cyclists`() {
+        assertFalse(
+            LeftAndRightCycleway(
+                CyclewayAndDirection(NONE, BACKWARD),
+                CyclewayAndDirection(NONE, FORWARD)
+            ).isNotOnewayForCyclistsNow(mapOf("oneway" to "yes"), false)
+        )
+    }
+
+    @Test fun `reverse oneway is oneway for cyclists`() {
+        assertFalse(
+            LeftAndRightCycleway(
+                CyclewayAndDirection(NONE, BACKWARD),
+                CyclewayAndDirection(NONE, FORWARD)
+            ).isNotOnewayForCyclistsNow(mapOf("oneway" to "-1"), false)
+        )
+    }
+
+    @Test fun `oneway is no oneway for cyclists when there is a dual track`() {
+        assertTrue(
+            LeftAndRightCycleway(
+                CyclewayAndDirection(NONE, BACKWARD),
+                CyclewayAndDirection(TRACK, BOTH)
+            ).isNotOnewayForCyclistsNow(mapOf("oneway" to "yes"), false)
+        )
+        assertTrue(
+            LeftAndRightCycleway(
+                CyclewayAndDirection(TRACK, BOTH),
+                CyclewayAndDirection(NONE, FORWARD)
+            ).isNotOnewayForCyclistsNow(mapOf("oneway" to "-1"), false)
+        )
+    }
+
+    @Test fun `oneway is no oneway for cyclists when any cycleway goes in contra-flow direction`() {
+        assertTrue(
+            LeftAndRightCycleway(CyclewayAndDirection(TRACK, BACKWARD), null)
+                .isNotOnewayForCyclistsNow(mapOf("oneway" to "yes"), false)
+        )
+        assertTrue(
+            LeftAndRightCycleway(null, CyclewayAndDirection(TRACK, BACKWARD))
+                .isNotOnewayForCyclistsNow(mapOf("oneway" to "yes"), false)
+        )
+        assertTrue(
+            LeftAndRightCycleway(CyclewayAndDirection(TRACK, FORWARD), null)
+                .isNotOnewayForCyclistsNow(mapOf("oneway" to "-1"), false)
+        )
+        assertTrue(
+            LeftAndRightCycleway(null, CyclewayAndDirection(TRACK, FORWARD))
+                .isNotOnewayForCyclistsNow(mapOf("oneway" to "-1"), false)
+        )
+    }
+
+    @Test fun `oneway is oneway for cyclists when no cycleway goes in contra-flow direction`() {
+        assertFalse(
+            LeftAndRightCycleway(CyclewayAndDirection(TRACK, FORWARD), null)
+                .isNotOnewayForCyclistsNow(mapOf("oneway" to "yes"), false)
+        )
+        assertFalse(
+            LeftAndRightCycleway(null, CyclewayAndDirection(TRACK, FORWARD))
+                .isNotOnewayForCyclistsNow(mapOf("oneway" to "yes"), false)
+        )
+        assertFalse(
+            LeftAndRightCycleway(CyclewayAndDirection(TRACK, BACKWARD), null)
+                .isNotOnewayForCyclistsNow(mapOf("oneway" to "-1"), false)
+        )
+        assertFalse(
+            LeftAndRightCycleway(null, CyclewayAndDirection(TRACK, BACKWARD))
+                .isNotOnewayForCyclistsNow(mapOf("oneway" to "-1"), false)
+        )
+    }
+}
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParserKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParserKtTest.kt
index bb32ff1fcb2..d86ba20cb2b 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParserKtTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParserKtTest.kt
@@ -1,23 +1,7 @@
 package de.westnordost.streetcomplete.osm.cycleway
 
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.ADVISORY_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.BUSWAY
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.DUAL_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.DUAL_TRACK
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.EXCLUSIVE_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.INVALID
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.NONE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.NONE_NO_ONEWAY
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.PICTOGRAMS
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SEPARATE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SIDEWALK_EXPLICIT
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SUGGESTION_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.TRACK
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNKNOWN
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNKNOWN_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNKNOWN_SHARED_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNSPECIFIED_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNSPECIFIED_SHARED_LANE
+import de.westnordost.streetcomplete.osm.cycleway.Cycleway.*
+import de.westnordost.streetcomplete.osm.cycleway.Direction.*
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNull
 import org.junit.Test
@@ -27,34 +11,132 @@ class CyclewayParserKtTest {
     *  make (much) assumptions that the code is written in a way that if it is solved for one type,
     *  it is solved for all */
 
+    /* -------------------------------------- special cases ------------------------------------- */
+
+    @Test fun `do not interpret non-oneway for bicycles as NONE_NO_ONEWAY if the cycleway on the other side is not a oneway`() {
+        assertEquals(
+            cycleway(NONE to BACKWARD, TRACK to BOTH),
+            parse(
+                "cycleway:right" to "track",
+                "cycleway:right:oneway" to "no",
+                "cycleway:left" to "no",
+                "oneway" to "yes",
+                "oneway:bicycle" to "no",
+            )
+        )
+        assertEquals(
+            cycleway(TRACK to BOTH, NONE to BACKWARD),
+            parseForLeftHandTraffic(
+                "cycleway:left" to "track",
+                "cycleway:left:oneway" to "no",
+                "cycleway:right" to "no",
+                "oneway" to "yes",
+                "oneway:bicycle" to "no",
+            )
+        )
+    }
+
+    @Test fun `fall back to bicycle=use_sidepath`() {
+        assertEquals(
+            cycleway(SEPARATE, SEPARATE),
+            parse("bicycle" to "use_sidepath")
+        )
+        assertEquals(
+            cycleway(NONE, SEPARATE),
+            parse("bicycle" to "use_sidepath", "cycleway:left" to "no")
+        )
+        assertEquals(
+            cycleway(SEPARATE, TRACK),
+            parse("bicycle" to "use_sidepath", "cycleway:right" to "track")
+        )
+    }
+
+    @Test fun `fall back to bicycle=use_sidepath with forward or backward`() {
+        assertEquals(
+            cycleway(null, SEPARATE),
+            parse("bicycle:forward" to "use_sidepath")
+        )
+        assertEquals(
+            cycleway(SEPARATE, null),
+            parse("bicycle:backward" to "use_sidepath")
+        )
+        assertEquals(
+            cycleway(SEPARATE, null, true),
+            parseForLeftHandTraffic("bicycle:forward" to "use_sidepath")
+        )
+        assertEquals(
+            cycleway(null, SEPARATE, true),
+            parseForLeftHandTraffic("bicycle:backward" to "use_sidepath")
+        )
+    }
+
+    /* ----------------------------------------- direction -------------------------------------- */
+
+    @Test fun `default directions`() {
+        assertEquals(
+            cycleway(NONE to BACKWARD, NONE to FORWARD),
+            parse("cycleway:both" to "no")
+        )
+        assertEquals(
+            cycleway(NONE to FORWARD, NONE to BACKWARD),
+            parseForLeftHandTraffic("cycleway:both" to "no")
+        )
+    }
+
+    @Test fun `fixed directions`() {
+        assertEquals(
+            cycleway(NONE to BACKWARD, NONE to BACKWARD),
+            parse("cycleway:both" to "no", "cycleway:both:oneway" to "-1")
+        )
+        assertEquals(
+            cycleway(NONE to FORWARD, NONE to FORWARD),
+            parse("cycleway:both" to "no", "cycleway:both:oneway" to "yes")
+        )
+        assertEquals(
+            cycleway(NONE to BOTH, NONE to BOTH),
+            parse("cycleway:both" to "no", "cycleway:both:oneway" to "no")
+        )
+    }
+
     /* ------------------------------------------ cycleway -------------------------------------- */
 
     @Test fun invalid() {
-        val invalid = LeftAndRightCycleway(INVALID, INVALID)
+        val invalid = cycleway(INVALID, INVALID)
         assertEquals(invalid, parse("cycleway" to "yes"))
         assertEquals(invalid, parse("cycleway" to "both"))
         assertEquals(invalid, parse("cycleway" to "left"))
         assertEquals(invalid, parse("cycleway" to "right"))
         assertEquals(invalid, parse("cycleway" to "shared"))
+        assertEquals(invalid, parse("cycleway" to "none"))
+
+        assertEquals(invalid, parse("cycleway" to "lane", "cycleway:lane" to "yes"))
+        assertEquals(invalid, parse("cycleway" to "lane", "cycleway:lane" to "right"))
+        assertEquals(invalid, parse("cycleway" to "lane", "cycleway:lane" to "left"))
+        assertEquals(invalid, parse("cycleway" to "lane", "cycleway:lane" to "both"))
+        assertEquals(invalid, parse("cycleway" to "lane", "cycleway:lane" to "shoulder"))
+        assertEquals(invalid, parse("cycleway" to "lane", "cycleway:lane" to "soft_lane"))
+        assertEquals(invalid, parse("cycleway" to "lane", "cycleway:lane" to "advisory_lane"))
+        assertEquals(invalid, parse("cycleway" to "lane", "cycleway:lane" to "exclusive_lane"))
+        assertEquals(invalid, parse("cycleway" to "lane", "cycleway:lane" to "mandatory"))
     }
 
     @Test fun unknown() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN, UNKNOWN),
+            cycleway(UNKNOWN, UNKNOWN),
             parse("cycleway" to "something")
         )
     }
 
     @Test fun `unknown in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNKNOWN),
+            cycleway(null, UNKNOWN),
             parse(
                 "cycleway" to "something",
                 "oneway" to "yes"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(null, UNKNOWN),
+            cycleway(null, UNKNOWN),
             parse(
                 "cycleway" to "something",
                 "junction" to "roundabout"
@@ -64,14 +146,14 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN, null),
+            cycleway(UNKNOWN, null),
             parse(
                 "cycleway" to "something",
                 "oneway" to "-1"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN, null),
+            cycleway(UNKNOWN, null),
             parse(
                 "cycleway" to "something",
                 "oneway" to "-1",
@@ -82,14 +164,14 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN, null),
+            cycleway(UNKNOWN, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "something",
                 "oneway" to "yes"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN, null),
+            cycleway(UNKNOWN, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "something",
                 "junction" to "roundabout"
@@ -99,14 +181,14 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNKNOWN),
+            cycleway(null, UNKNOWN, true),
             parseForLeftHandTraffic(
                 "cycleway" to "something",
                 "oneway" to "-1"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(null, UNKNOWN),
+            cycleway(null, UNKNOWN, true),
             parseForLeftHandTraffic(
                 "cycleway" to "something",
                 "oneway" to "-1",
@@ -117,7 +199,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown cycle lane`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN_LANE, UNKNOWN_LANE),
+            cycleway(UNKNOWN_LANE, UNKNOWN_LANE),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "something"
@@ -127,7 +209,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown cycle lane in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNKNOWN_LANE),
+            cycleway(null, UNKNOWN_LANE),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "something",
@@ -138,7 +220,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown cycle lane in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN_LANE, null),
+            cycleway(UNKNOWN_LANE, null),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "something",
@@ -149,7 +231,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown cycle lane in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN_LANE, null),
+            cycleway(UNKNOWN_LANE, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "lane",
                 "cycleway:lane" to "something",
@@ -160,7 +242,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown cycle lane in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNKNOWN_LANE),
+            cycleway(null, UNKNOWN_LANE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "lane",
                 "cycleway:lane" to "something",
@@ -171,7 +253,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown shared lane`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN_SHARED_LANE, UNKNOWN_SHARED_LANE),
+            cycleway(UNKNOWN_SHARED_LANE, UNKNOWN_SHARED_LANE),
             parse(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "something"
@@ -181,7 +263,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown shared lane in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNKNOWN_SHARED_LANE),
+            cycleway(null, UNKNOWN_SHARED_LANE),
             parse(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "something",
@@ -192,7 +274,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown shared lane in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN_SHARED_LANE, null),
+            cycleway(UNKNOWN_SHARED_LANE, null),
             parse(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "something",
@@ -203,7 +285,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown shared lane in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN_SHARED_LANE, null),
+            cycleway(UNKNOWN_SHARED_LANE, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "something",
@@ -214,7 +296,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown shared lane in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNKNOWN_SHARED_LANE),
+            cycleway(null, UNKNOWN_SHARED_LANE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "something",
@@ -225,14 +307,14 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified shared lane`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_SHARED_LANE, UNSPECIFIED_SHARED_LANE),
+            cycleway(UNSPECIFIED_SHARED_LANE, UNSPECIFIED_SHARED_LANE),
             parse("cycleway" to "shared_lane")
         )
     }
 
     @Test fun `unspecified shared lane in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNSPECIFIED_SHARED_LANE),
+            cycleway(null, UNSPECIFIED_SHARED_LANE),
             parse(
                 "cycleway" to "shared_lane",
                 "oneway" to "yes"
@@ -242,7 +324,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified shared lane in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_SHARED_LANE, null),
+            cycleway(UNSPECIFIED_SHARED_LANE, null),
             parse(
                 "cycleway" to "shared_lane",
                 "oneway" to "-1"
@@ -252,7 +334,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified shared lane in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_SHARED_LANE, null),
+            cycleway(UNSPECIFIED_SHARED_LANE, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "shared_lane",
                 "oneway" to "yes"
@@ -262,7 +344,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified shared lane in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNSPECIFIED_SHARED_LANE),
+            cycleway(null, UNSPECIFIED_SHARED_LANE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "shared_lane",
                 "oneway" to "-1"
@@ -272,14 +354,14 @@ class CyclewayParserKtTest {
 
     @Test fun track() {
         assertEquals(
-            LeftAndRightCycleway(TRACK, TRACK),
+            cycleway(TRACK, TRACK),
             parse("cycleway" to "track")
         )
     }
 
     @Test fun `track in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, TRACK),
+            cycleway(null, TRACK),
             parse(
                 "cycleway" to "track",
                 "oneway" to "yes"
@@ -289,7 +371,7 @@ class CyclewayParserKtTest {
 
     @Test fun `track in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(TRACK, null),
+            cycleway(TRACK, null),
             parse(
                 "cycleway" to "track",
                 "oneway" to "-1"
@@ -299,7 +381,7 @@ class CyclewayParserKtTest {
 
     @Test fun `track in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(TRACK, null),
+            cycleway(TRACK, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "track",
                 "oneway" to "yes"
@@ -309,7 +391,7 @@ class CyclewayParserKtTest {
 
     @Test fun `track in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, TRACK),
+            cycleway(null, TRACK, true),
             parseForLeftHandTraffic(
                 "cycleway" to "track",
                 "oneway" to "-1"
@@ -319,7 +401,7 @@ class CyclewayParserKtTest {
 
     @Test fun `explicitly on sidewalk`() {
         assertEquals(
-            LeftAndRightCycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
+            cycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway" to "track",
                 "cycleway:segregated" to "no"
@@ -329,7 +411,7 @@ class CyclewayParserKtTest {
 
     @Test fun `explicitly on sidewalk in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, SIDEWALK_EXPLICIT),
+            cycleway(null, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway" to "track",
                 "cycleway:segregated" to "no",
@@ -340,7 +422,7 @@ class CyclewayParserKtTest {
 
     @Test fun `explicitly on sidewalk in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(SIDEWALK_EXPLICIT, null),
+            cycleway(SIDEWALK_EXPLICIT, null),
             parse(
                 "cycleway" to "track",
                 "cycleway:segregated" to "no",
@@ -351,7 +433,7 @@ class CyclewayParserKtTest {
 
     @Test fun `explicitly on sidewalk in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(SIDEWALK_EXPLICIT, null),
+            cycleway(SIDEWALK_EXPLICIT, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "track",
                 "cycleway:segregated" to "no",
@@ -362,7 +444,7 @@ class CyclewayParserKtTest {
 
     @Test fun `explicitly on sidewalk in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, SIDEWALK_EXPLICIT),
+            cycleway(null, SIDEWALK_EXPLICIT, true),
             parseForLeftHandTraffic(
                 "cycleway" to "track",
                 "cycleway:segregated" to "no",
@@ -373,7 +455,7 @@ class CyclewayParserKtTest {
 
     @Test fun `dual track`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, DUAL_TRACK),
+            cycleway(TRACK to BOTH, TRACK to BOTH),
             parse(
                 "cycleway" to "track",
                 "cycleway:oneway" to "no"
@@ -383,7 +465,7 @@ class CyclewayParserKtTest {
 
     @Test fun `dual track in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_TRACK),
+            cycleway(null, TRACK to BOTH),
             parse(
                 "cycleway" to "track",
                 "cycleway:oneway" to "no",
@@ -394,7 +476,7 @@ class CyclewayParserKtTest {
 
     @Test fun `dual track in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, null),
+            cycleway(TRACK to BOTH, null),
             parse(
                 "cycleway" to "track",
                 "cycleway:oneway" to "no",
@@ -405,7 +487,7 @@ class CyclewayParserKtTest {
 
     @Test fun `dual track in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, null),
+            cycleway(TRACK to BOTH, null),
             parseForLeftHandTraffic(
                 "cycleway" to "track",
                 "cycleway:oneway" to "no",
@@ -416,7 +498,7 @@ class CyclewayParserKtTest {
 
     @Test fun `dual track in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_TRACK),
+            cycleway(null, TRACK to BOTH),
             parseForLeftHandTraffic(
                 "cycleway" to "track",
                 "cycleway:oneway" to "no",
@@ -427,14 +509,14 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified lane`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_LANE, UNSPECIFIED_LANE),
+            cycleway(UNSPECIFIED_LANE, UNSPECIFIED_LANE),
             parse("cycleway" to "lane")
         )
     }
 
     @Test fun `unspecified lane in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNSPECIFIED_LANE),
+            cycleway(null, UNSPECIFIED_LANE),
             parse(
                 "cycleway" to "lane",
                 "oneway" to "yes"
@@ -444,7 +526,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified lane in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_LANE, null),
+            cycleway(UNSPECIFIED_LANE, null),
             parse(
                 "cycleway" to "lane",
                 "oneway" to "-1"
@@ -454,7 +536,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified lane in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_LANE, null),
+            cycleway(UNSPECIFIED_LANE, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "lane",
                 "oneway" to "yes"
@@ -464,7 +546,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified lane in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNSPECIFIED_LANE),
+            cycleway(null, UNSPECIFIED_LANE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "lane",
                 "oneway" to "-1"
@@ -474,7 +556,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified dual lane`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
+            cycleway(UNSPECIFIED_LANE to BOTH, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway" to "lane",
                 "cycleway:oneway" to "no"
@@ -484,7 +566,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive lane`() {
         assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
+            cycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "exclusive"
@@ -494,7 +576,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive lane in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, EXCLUSIVE_LANE),
+            cycleway(null, EXCLUSIVE_LANE),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "exclusive",
@@ -505,7 +587,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive lane in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, null),
+            cycleway(EXCLUSIVE_LANE, null),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "exclusive",
@@ -516,7 +598,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive lane in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, null),
+            cycleway(EXCLUSIVE_LANE, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "lane",
                 "cycleway:lane" to "exclusive",
@@ -527,7 +609,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive lane in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, EXCLUSIVE_LANE),
+            cycleway(null, EXCLUSIVE_LANE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "lane",
                 "cycleway:lane" to "exclusive",
@@ -536,26 +618,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive lane synonyms`() {
-        assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
-            parse(
-                "cycleway" to "lane",
-                "cycleway:lane" to "exclusive_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
-            parse(
-                "cycleway" to "lane",
-                "cycleway:lane" to "mandatory"
-            )
-        )
-    }
-
     @Test fun `exclusive dual lane`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
+            cycleway(EXCLUSIVE_LANE to BOTH, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "exclusive",
@@ -566,7 +631,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive dual lane in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "exclusive",
@@ -578,7 +643,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive dual lane in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(EXCLUSIVE_LANE to BOTH, null),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "exclusive",
@@ -590,7 +655,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive dual lane in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(EXCLUSIVE_LANE to BOTH, null),
             parseForLeftHandTraffic(
                 "cycleway" to "lane",
                 "cycleway:lane" to "exclusive",
@@ -602,7 +667,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive dual lane in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, EXCLUSIVE_LANE to BOTH),
             parseForLeftHandTraffic(
                 "cycleway" to "lane",
                 "cycleway:lane" to "exclusive",
@@ -612,28 +677,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive dual lane synonyms`() {
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
-            parse(
-                "cycleway" to "lane",
-                "cycleway:lane" to "exclusive_lane",
-                "cycleway:oneway" to "no"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
-            parse(
-                "cycleway" to "lane",
-                "cycleway:lane" to "mandatory",
-                "cycleway:oneway" to "no"
-            )
-        )
-    }
-
     @Test fun `advisory lane`() {
         assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
+            cycleway(ADVISORY_LANE, ADVISORY_LANE),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "advisory"
@@ -643,7 +689,7 @@ class CyclewayParserKtTest {
 
     @Test fun `advisory lane in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
+            cycleway(null, ADVISORY_LANE),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "advisory",
@@ -654,7 +700,7 @@ class CyclewayParserKtTest {
 
     @Test fun `advisory lane in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
+            cycleway(ADVISORY_LANE, null),
             parse(
                 "cycleway" to "lane",
                 "cycleway:lane" to "advisory",
@@ -665,7 +711,7 @@ class CyclewayParserKtTest {
 
     @Test fun `advisory lane in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
+            cycleway(ADVISORY_LANE, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "lane",
                 "cycleway:lane" to "advisory",
@@ -676,7 +722,7 @@ class CyclewayParserKtTest {
 
     @Test fun `advisory lane in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
+            cycleway(null, ADVISORY_LANE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "lane",
                 "cycleway:lane" to "advisory",
@@ -685,33 +731,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `advisory lane synonyms`() {
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
-            parse(
-                "cycleway" to "lane",
-                "cycleway:lane" to "advisory_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
-            parse(
-                "cycleway" to "lane",
-                "cycleway:lane" to "soft_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
-            parse(
-                "cycleway" to "lane",
-                "cycleway:lane" to "dashed"
-            )
-        )
-    }
-
     @Test fun `suggestion lane`() {
         assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, SUGGESTION_LANE),
+            cycleway(SUGGESTION_LANE, SUGGESTION_LANE),
             parse(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "advisory"
@@ -721,7 +743,7 @@ class CyclewayParserKtTest {
 
     @Test fun `suggestion lane in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, SUGGESTION_LANE),
+            cycleway(null, SUGGESTION_LANE),
             parse(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "advisory",
@@ -732,7 +754,7 @@ class CyclewayParserKtTest {
 
     @Test fun `suggestion lane in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, null),
+            cycleway(SUGGESTION_LANE, null),
             parse(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "advisory",
@@ -743,7 +765,7 @@ class CyclewayParserKtTest {
 
     @Test fun `suggestion lane in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, null),
+            cycleway(SUGGESTION_LANE, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "advisory",
@@ -754,7 +776,7 @@ class CyclewayParserKtTest {
 
     @Test fun `suggestion lane in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, SUGGESTION_LANE),
+            cycleway(null, SUGGESTION_LANE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "advisory",
@@ -763,33 +785,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `suggestion lane synonyms`() {
-        assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, SUGGESTION_LANE),
-            parse(
-                "cycleway" to "shared_lane",
-                "cycleway:lane" to "advisory_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, SUGGESTION_LANE),
-            parse(
-                "cycleway" to "shared_lane",
-                "cycleway:lane" to "soft_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, SUGGESTION_LANE),
-            parse(
-                "cycleway" to "shared_lane",
-                "cycleway:lane" to "dashed"
-            )
-        )
-    }
-
     @Test fun pictograms() {
         assertEquals(
-            LeftAndRightCycleway(PICTOGRAMS, PICTOGRAMS),
+            cycleway(PICTOGRAMS, PICTOGRAMS),
             parse(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "pictogram"
@@ -799,7 +797,7 @@ class CyclewayParserKtTest {
 
     @Test fun `pictograms in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, PICTOGRAMS),
+            cycleway(null, PICTOGRAMS),
             parse(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "pictogram",
@@ -810,7 +808,7 @@ class CyclewayParserKtTest {
 
     @Test fun `pictograms in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(PICTOGRAMS, null),
+            cycleway(PICTOGRAMS, null),
             parse(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "pictogram",
@@ -821,7 +819,7 @@ class CyclewayParserKtTest {
 
     @Test fun `pictograms in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(PICTOGRAMS, null),
+            cycleway(PICTOGRAMS, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "pictogram",
@@ -832,7 +830,7 @@ class CyclewayParserKtTest {
 
     @Test fun `pictograms in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, PICTOGRAMS),
+            cycleway(null, PICTOGRAMS, true),
             parseForLeftHandTraffic(
                 "cycleway" to "shared_lane",
                 "cycleway:lane" to "pictogram",
@@ -843,60 +841,56 @@ class CyclewayParserKtTest {
 
     @Test fun none() {
         assertEquals(
-            LeftAndRightCycleway(NONE, NONE),
+            cycleway(NONE, NONE),
             parse("cycleway" to "no")
         )
-        assertEquals(
-            LeftAndRightCycleway(NONE, NONE),
-            parse("cycleway" to "none")
-        )
     }
 
     @Test fun separate() {
         assertEquals(
-            LeftAndRightCycleway(SEPARATE, SEPARATE),
+            cycleway(SEPARATE, SEPARATE),
             parse("cycleway" to "separate")
         )
     }
 
     @Test fun busway() {
         assertEquals(
-            LeftAndRightCycleway(BUSWAY, BUSWAY),
+            cycleway(BUSWAY, BUSWAY),
             parse("cycleway" to "share_busway")
         )
     }
 
     @Test fun `busway in oneway`() {
         assertEquals(
-            LeftAndRightCycleway(null, BUSWAY),
+            cycleway(null, BUSWAY),
             parse("cycleway" to "share_busway", "oneway" to "yes")
         )
     }
 
     @Test fun `busway in oneway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(BUSWAY, null),
+            cycleway(BUSWAY, null),
             parse("cycleway" to "share_busway", "oneway" to "-1")
         )
     }
 
     @Test fun `busway in oneway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(BUSWAY, null),
+            cycleway(BUSWAY, null, true),
             parseForLeftHandTraffic("cycleway" to "share_busway", "oneway" to "yes")
         )
     }
 
     @Test fun `busway in oneway (reversed, left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, BUSWAY),
+            cycleway(null, BUSWAY, true),
             parseForLeftHandTraffic("cycleway" to "share_busway", "oneway" to "-1")
         )
     }
 
     @Test fun `none but oneway that isn't a oneway for cyclists`() {
         assertEquals(
-            LeftAndRightCycleway(NONE_NO_ONEWAY, NONE),
+            cycleway(NONE_NO_ONEWAY, NONE),
             parse(
                 "cycleway" to "no",
                 "oneway" to "yes",
@@ -907,7 +901,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none but oneway that isn't a oneway for cyclists (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, NONE_NO_ONEWAY),
+            cycleway(NONE, NONE_NO_ONEWAY),
             parse(
                 "cycleway" to "no",
                 "oneway" to "-1",
@@ -918,7 +912,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none but oneway that isn't a oneway for cyclists (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, NONE_NO_ONEWAY),
+            cycleway(NONE, NONE_NO_ONEWAY, true),
             parseForLeftHandTraffic(
                 "cycleway" to "no",
                 "oneway" to "yes",
@@ -929,7 +923,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none but oneway that isn't a oneway for cyclists (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE_NO_ONEWAY, NONE),
+            cycleway(NONE_NO_ONEWAY, NONE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "no",
                 "oneway" to "-1",
@@ -938,11 +932,25 @@ class CyclewayParserKtTest {
         )
     }
 
+    @Test fun shoulder() {
+        assertEquals(
+            cycleway(SHOULDER, SHOULDER),
+            parse("cycleway" to "shoulder")
+        )
+    }
+
+    @Test fun `shoulder in oneway`() {
+        assertEquals(
+            cycleway(null, SHOULDER),
+            parse("cycleway" to "shoulder", "oneway" to "yes")
+        )
+    }
+
     /* ------------------------------ cycleway opposite taggings -------------------------------- */
 
     @Test fun `cycleway opposite`() {
         assertEquals(
-            LeftAndRightCycleway(NONE_NO_ONEWAY, NONE),
+            cycleway(NONE_NO_ONEWAY, NONE),
             parse(
                 "cycleway" to "opposite",
                 "oneway" to "yes"
@@ -952,7 +960,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, NONE_NO_ONEWAY),
+            cycleway(NONE, NONE_NO_ONEWAY),
             parse(
                 "cycleway" to "opposite",
                 "oneway" to "-1"
@@ -962,7 +970,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, NONE_NO_ONEWAY),
+            cycleway(NONE, NONE_NO_ONEWAY, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite",
                 "oneway" to "yes"
@@ -972,7 +980,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE_NO_ONEWAY, NONE),
+            cycleway(NONE_NO_ONEWAY, NONE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite",
                 "oneway" to "-1"
@@ -982,7 +990,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite track`() {
         assertEquals(
-            LeftAndRightCycleway(TRACK, null),
+            cycleway(TRACK, null),
             parse(
                 "cycleway" to "opposite_track",
                 "oneway" to "yes"
@@ -992,7 +1000,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite track (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(null, TRACK),
+            cycleway(null, TRACK),
             parse(
                 "cycleway" to "opposite_track",
                 "oneway" to "-1"
@@ -1002,7 +1010,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite track (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, TRACK),
+            cycleway(null, TRACK, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_track",
                 "oneway" to "yes"
@@ -1012,7 +1020,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite track (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(TRACK, null),
+            cycleway(TRACK, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_track",
                 "oneway" to "-1"
@@ -1022,7 +1030,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite dual track`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, null),
+            cycleway(TRACK to BOTH, null),
             parse(
                 "cycleway" to "opposite_track",
                 "oneway" to "yes",
@@ -1033,7 +1041,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite dual track (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_TRACK),
+            cycleway(null, TRACK to BOTH),
             parse(
                 "cycleway" to "opposite_track",
                 "oneway" to "-1",
@@ -1044,7 +1052,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite dual track (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_TRACK),
+            cycleway(null, TRACK to BOTH),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_track",
                 "oneway" to "yes",
@@ -1055,7 +1063,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite dual track (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, null),
+            cycleway(TRACK to BOTH, null),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_track",
                 "oneway" to "-1",
@@ -1066,7 +1074,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite busway`() {
         assertEquals(
-            LeftAndRightCycleway(BUSWAY, null),
+            cycleway(BUSWAY, null),
             parse(
                 "cycleway" to "opposite_share_busway",
                 "oneway" to "yes"
@@ -1076,7 +1084,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite busway (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(null, BUSWAY),
+            cycleway(null, BUSWAY),
             parse(
                 "cycleway" to "opposite_share_busway",
                 "oneway" to "-1"
@@ -1086,7 +1094,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite busway (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, BUSWAY),
+            cycleway(null, BUSWAY, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_share_busway",
                 "oneway" to "yes"
@@ -1096,7 +1104,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite busway (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(BUSWAY, null),
+            cycleway(BUSWAY, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_share_busway",
                 "oneway" to "-1"
@@ -1106,7 +1114,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite unspecified lane`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_LANE, null),
+            cycleway(UNSPECIFIED_LANE, null),
             parse(
                 "cycleway" to "opposite_lane",
                 "oneway" to "yes"
@@ -1116,7 +1124,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite unspecified lane (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNSPECIFIED_LANE),
+            cycleway(null, UNSPECIFIED_LANE),
             parse(
                 "cycleway" to "opposite_lane",
                 "oneway" to "-1"
@@ -1126,7 +1134,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite unspecified lane (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNSPECIFIED_LANE),
+            cycleway(null, UNSPECIFIED_LANE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_lane",
                 "oneway" to "yes"
@@ -1136,7 +1144,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite unspecified lane (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_LANE, null),
+            cycleway(UNSPECIFIED_LANE, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_lane",
                 "oneway" to "-1"
@@ -1146,7 +1154,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite unspecified dual lane`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(UNSPECIFIED_LANE to BOTH, null),
             parse(
                 "cycleway" to "opposite_lane",
                 "oneway" to "yes",
@@ -1157,7 +1165,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite unspecified dual lane (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway" to "opposite_lane",
                 "oneway" to "-1",
@@ -1168,7 +1176,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite unspecified dual lane (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, UNSPECIFIED_LANE to BOTH),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_lane",
                 "oneway" to "yes",
@@ -1179,7 +1187,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite unspecified dual lane (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(UNSPECIFIED_LANE to BOTH, null),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_lane",
                 "oneway" to "-1",
@@ -1190,7 +1198,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite advisory lane`() {
         assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
+            cycleway(ADVISORY_LANE, null),
             parse(
                 "cycleway" to "opposite_lane",
                 "oneway" to "yes",
@@ -1201,7 +1209,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite advisory lane (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
+            cycleway(null, ADVISORY_LANE),
             parse(
                 "cycleway" to "opposite_lane",
                 "oneway" to "-1",
@@ -1212,7 +1220,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite advisory lane (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
+            cycleway(null, ADVISORY_LANE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_lane",
                 "oneway" to "yes",
@@ -1223,7 +1231,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite advisory lane (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
+            cycleway(ADVISORY_LANE, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_lane",
                 "oneway" to "-1",
@@ -1234,7 +1242,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite exclusive lane`() {
         assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, null),
+            cycleway(EXCLUSIVE_LANE, null),
             parse(
                 "cycleway" to "opposite_lane",
                 "oneway" to "yes",
@@ -1245,7 +1253,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite exclusive lane (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(null, EXCLUSIVE_LANE),
+            cycleway(null, EXCLUSIVE_LANE),
             parse(
                 "cycleway" to "opposite_lane",
                 "oneway" to "-1",
@@ -1256,7 +1264,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite exclusive lane (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, EXCLUSIVE_LANE),
+            cycleway(null, EXCLUSIVE_LANE, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_lane",
                 "oneway" to "yes",
@@ -1267,7 +1275,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite exclusive lane (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, null),
+            cycleway(EXCLUSIVE_LANE, null, true),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_lane",
                 "oneway" to "-1",
@@ -1278,7 +1286,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite exclusive dual lane`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(EXCLUSIVE_LANE to BOTH, null),
             parse(
                 "cycleway" to "opposite_lane",
                 "oneway" to "yes",
@@ -1290,7 +1298,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite exclusive dual lane (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway" to "opposite_lane",
                 "oneway" to "-1",
@@ -1302,7 +1310,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite exclusive dual lane (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, EXCLUSIVE_LANE to BOTH),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_lane",
                 "oneway" to "yes",
@@ -1314,7 +1322,7 @@ class CyclewayParserKtTest {
 
     @Test fun `cycleway opposite exclusive dual lane (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(EXCLUSIVE_LANE to BOTH, null),
             parseForLeftHandTraffic(
                 "cycleway" to "opposite_lane",
                 "oneway" to "-1",
@@ -1328,14 +1336,14 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown on left side`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN, null),
+            cycleway(UNKNOWN, null),
             parse("cycleway:left" to "something")
         )
     }
 
     @Test fun `unknown cycle lane on left side`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN_LANE, null),
+            cycleway(UNKNOWN_LANE, null),
             parse(
                 "cycleway:left" to "lane",
                 "cycleway:left:lane" to "something"
@@ -1345,7 +1353,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown shared lane on left side`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN_SHARED_LANE, null),
+            cycleway(UNKNOWN_SHARED_LANE, null),
             parse(
                 "cycleway:left" to "shared_lane",
                 "cycleway:left:lane" to "something"
@@ -1355,21 +1363,21 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified shared lane on left side`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_SHARED_LANE, null),
+            cycleway(UNSPECIFIED_SHARED_LANE, null),
             parse("cycleway:left" to "shared_lane")
         )
     }
 
     @Test fun `track left`() {
         assertEquals(
-            LeftAndRightCycleway(TRACK, null),
+            cycleway(TRACK, null),
             parse("cycleway:left" to "track")
         )
     }
 
     @Test fun `explicitly on sidewalk on left side`() {
         assertEquals(
-            LeftAndRightCycleway(SIDEWALK_EXPLICIT, null),
+            cycleway(SIDEWALK_EXPLICIT, null),
             parse(
                 "cycleway:left" to "track",
                 "cycleway:left:segregated" to "no"
@@ -1379,21 +1387,21 @@ class CyclewayParserKtTest {
 
     @Test fun `dual track on left side`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, null),
+            cycleway(TRACK to BOTH, null),
             parse(
                 "cycleway:left" to "track",
                 "cycleway:left:oneway" to "no"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, null),
+            cycleway(TRACK to BOTH, null),
             parse(
                 "cycleway:left" to "track",
                 "cycleway:both:oneway" to "no"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, null),
+            cycleway(TRACK to BOTH, null),
             parse(
                 "cycleway:left" to "track",
                 "cycleway:oneway" to "no"
@@ -1403,14 +1411,14 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified lane on left side`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_LANE, null),
+            cycleway(UNSPECIFIED_LANE, null),
             parse("cycleway:left" to "lane")
         )
     }
 
     @Test fun `unspecified dual lane on left side`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(UNSPECIFIED_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "lane",
                 "cycleway:left:oneway" to "no"
@@ -1418,14 +1426,14 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(UNSPECIFIED_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "lane",
                 "cycleway:both:oneway" to "no"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(UNSPECIFIED_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "lane",
                 "cycleway:oneway" to "no"
@@ -1435,7 +1443,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive lane on left side`() {
         assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, null),
+            cycleway(EXCLUSIVE_LANE, null),
             parse(
                 "cycleway:left" to "lane",
                 "cycleway:left:lane" to "exclusive"
@@ -1443,26 +1451,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive lane synonyms on left side`() {
-        assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, null),
-            parse(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "exclusive_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, null),
-            parse(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "mandatory"
-            )
-        )
-    }
-
     @Test fun `exclusive dual lane on left side`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(EXCLUSIVE_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "lane",
                 "cycleway:left:lane" to "exclusive",
@@ -1470,7 +1461,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(EXCLUSIVE_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "lane",
                 "cycleway:left:lane" to "exclusive",
@@ -1478,7 +1469,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(EXCLUSIVE_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "lane",
                 "cycleway:left:lane" to "exclusive",
@@ -1487,61 +1478,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive dual lane synonyms on left side`() {
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
-            parse(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "exclusive_lane",
-                "cycleway:left:oneway" to "no"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
-            parse(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "mandatory",
-                "cycleway:left:oneway" to "no"
-            )
-        )
-
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
-            parse(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "exclusive_lane",
-                "cycleway:both:oneway" to "no"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
-            parse(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "mandatory",
-                "cycleway:both:oneway" to "no"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
-            parse(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "exclusive_lane",
-                "cycleway:oneway" to "no"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
-            parse(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "mandatory",
-                "cycleway:oneway" to "no"
-            )
-        )
-    }
-
     @Test fun `advisory lane on left side`() {
         assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
+            cycleway(ADVISORY_LANE, null),
             parse(
                 "cycleway:left" to "lane",
                 "cycleway:left:lane" to "advisory"
@@ -1549,33 +1488,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `advisory lane synonyms on left side`() {
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
-            parse(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "advisory_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
-            parse(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "soft_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
-            parse(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "dashed"
-            )
-        )
-    }
-
     @Test fun `suggestion lane on left side`() {
         assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, null),
+            cycleway(SUGGESTION_LANE, null),
             parse(
                 "cycleway:left" to "shared_lane",
                 "cycleway:left:lane" to "advisory"
@@ -1583,33 +1498,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `suggestion lane synonyms on left side`() {
-        assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, null),
-            parse(
-                "cycleway:left" to "shared_lane",
-                "cycleway:left:lane" to "advisory_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, null),
-            parse(
-                "cycleway:left" to "shared_lane",
-                "cycleway:left:lane" to "soft_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, null),
-            parse(
-                "cycleway:left" to "shared_lane",
-                "cycleway:left:lane" to "dashed"
-            )
-        )
-    }
-
     @Test fun `pictograms on left side`() {
         assertEquals(
-            LeftAndRightCycleway(PICTOGRAMS, null),
+            cycleway(PICTOGRAMS, null),
             parse(
                 "cycleway:left" to "shared_lane",
                 "cycleway:left:lane" to "pictogram"
@@ -1619,32 +1510,28 @@ class CyclewayParserKtTest {
 
     @Test fun `none on left side`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, null),
+            cycleway(NONE, null),
             parse("cycleway:left" to "no")
         )
-        assertEquals(
-            LeftAndRightCycleway(NONE, null),
-            parse("cycleway:left" to "none")
-        )
     }
 
     @Test fun `separate on left side`() {
         assertEquals(
-            LeftAndRightCycleway(SEPARATE, null),
+            cycleway(SEPARATE, null),
             parse("cycleway:left" to "separate")
         )
     }
 
     @Test fun `busway on left side`() {
         assertEquals(
-            LeftAndRightCycleway(BUSWAY, null),
+            cycleway(BUSWAY, null),
             parse("cycleway:left" to "share_busway")
         )
     }
 
     @Test fun `none on left side but oneway that isn't a oneway for cyclists`() {
         assertEquals(
-            LeftAndRightCycleway(NONE_NO_ONEWAY, null),
+            cycleway(NONE_NO_ONEWAY, null),
             parse(
                 "cycleway:left" to "no",
                 "oneway" to "yes",
@@ -1655,7 +1542,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none on left side but oneway that isn't a oneway for cyclists (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, NONE_NO_ONEWAY),
+            cycleway(NONE, NONE_NO_ONEWAY),
             parse(
                 "cycleway:left" to "no",
                 "oneway" to "-1",
@@ -1666,7 +1553,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none on left side but oneway that isn't a oneway for cyclists (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, NONE_NO_ONEWAY),
+            cycleway(NONE, NONE_NO_ONEWAY, true),
             parseForLeftHandTraffic(
                 "cycleway:left" to "no",
                 "oneway" to "yes",
@@ -1677,7 +1564,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none on left side but oneway that isn't a oneway for cyclists (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE_NO_ONEWAY, null),
+            cycleway(NONE_NO_ONEWAY, null, true),
             parseForLeftHandTraffic(
                 "cycleway:left" to "no",
                 "oneway" to "-1",
@@ -1686,11 +1573,18 @@ class CyclewayParserKtTest {
         )
     }
 
+    @Test fun `shoulder on left side`() {
+        assertEquals(
+            cycleway(SHOULDER, null),
+            parse("cycleway:left" to "shoulder")
+        )
+    }
+
     /* ------------------------------ cycleway:left opposite tagging --------------------------- */
 
     @Test fun `left opposite`() {
         assertEquals(
-            LeftAndRightCycleway(NONE_NO_ONEWAY, null),
+            cycleway(NONE_NO_ONEWAY, null),
             parse(
                 "cycleway:left" to "opposite",
                 "oneway" to "yes"
@@ -1700,7 +1594,7 @@ class CyclewayParserKtTest {
 
     @Test fun `left opposite (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, null),
+            cycleway(NONE, null, true),
             parseForLeftHandTraffic(
                 "cycleway:left" to "opposite",
                 "oneway" to "yes"
@@ -1710,7 +1604,7 @@ class CyclewayParserKtTest {
 
     @Test fun `track left opposite`() {
         assertEquals(
-            LeftAndRightCycleway(TRACK, null),
+            cycleway(TRACK, null),
             parse(
                 "cycleway:left" to "opposite_track",
                 "oneway" to "yes"
@@ -1720,7 +1614,7 @@ class CyclewayParserKtTest {
 
     @Test fun `explicitly on sidewalk on left side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(SIDEWALK_EXPLICIT, null),
+            cycleway(SIDEWALK_EXPLICIT, null),
             parse(
                 "cycleway:left" to "opposite_track",
                 "cycleway:left:segregated" to "no",
@@ -1728,7 +1622,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(SIDEWALK_EXPLICIT, null),
+            cycleway(SIDEWALK_EXPLICIT, null),
             parse(
                 "cycleway:left" to "opposite_track",
                 "cycleway:both:segregated" to "no",
@@ -1736,7 +1630,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(SIDEWALK_EXPLICIT, null),
+            cycleway(SIDEWALK_EXPLICIT, null),
             parse(
                 "cycleway:left" to "opposite_track",
                 "cycleway:segregated" to "no",
@@ -1747,7 +1641,7 @@ class CyclewayParserKtTest {
 
     @Test fun `dual track on left side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, null),
+            cycleway(TRACK to BOTH, null),
             parse(
                 "cycleway:left" to "opposite_track",
                 "cycleway:left:oneway" to "no",
@@ -1755,7 +1649,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, null),
+            cycleway(TRACK to BOTH, null),
             parse(
                 "cycleway:left" to "opposite_track",
                 "cycleway:both:oneway" to "no",
@@ -1763,7 +1657,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, null),
+            cycleway(TRACK to BOTH, null),
             parse(
                 "cycleway:left" to "opposite_track",
                 "cycleway:oneway" to "no",
@@ -1774,7 +1668,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified lane on left side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_LANE, null),
+            cycleway(UNSPECIFIED_LANE, null),
             parse(
                 "cycleway:left" to "opposite_lane",
                 "oneway" to "yes"
@@ -1784,7 +1678,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified dual lane on left side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(UNSPECIFIED_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "opposite_lane",
                 "cycleway:left:oneway" to "no",
@@ -1793,7 +1687,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(UNSPECIFIED_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "opposite_lane",
                 "cycleway:both:oneway" to "no",
@@ -1801,7 +1695,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(UNSPECIFIED_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "opposite_lane",
                 "cycleway:oneway" to "no",
@@ -1812,7 +1706,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive lane on left side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, null),
+            cycleway(EXCLUSIVE_LANE, null),
             parse(
                 "cycleway:left" to "opposite_lane",
                 "cycleway:left:lane" to "exclusive",
@@ -1821,28 +1715,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive lane synonyms on left side opposite`() {
-        assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, null),
-            parse(
-                "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "exclusive_lane",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, null),
-            parse(
-                "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "mandatory",
-                "oneway" to "yes"
-            )
-        )
-    }
-
     @Test fun `exclusive dual lane on left side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(EXCLUSIVE_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "opposite_lane",
                 "cycleway:left:lane" to "exclusive",
@@ -1852,7 +1727,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(EXCLUSIVE_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "opposite_lane",
                 "cycleway:left:lane" to "exclusive",
@@ -1861,7 +1736,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(EXCLUSIVE_LANE to BOTH, null),
             parse(
                 "cycleway:left" to "opposite_lane",
                 "cycleway:left:lane" to "exclusive",
@@ -1871,135 +1746,49 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive dual lane synonyms on left side opposite`() {
+    @Test fun `advisory lane on left side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(ADVISORY_LANE, null),
             parse(
                 "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "exclusive_lane",
-                "cycleway:left:oneway" to "no",
+                "cycleway:left:lane" to "advisory",
                 "oneway" to "yes"
             )
         )
+    }
+
+    @Test fun `busway on left side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(BUSWAY, null),
             parse(
-                "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "mandatory",
-                "cycleway:left:oneway" to "no",
+                "cycleway:left" to "opposite_share_busway",
                 "oneway" to "yes"
             )
         )
+    }
+
+    /* -------------------------------------- cycleway:right  ----------------------------------- */
 
+    @Test fun `unknown on right side`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
-            parse(
-                "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "exclusive_lane",
-                "cycleway:both:oneway" to "no",
-                "oneway" to "yes"
-            )
+            cycleway(null, UNKNOWN),
+            parse("cycleway:right" to "something")
         )
+    }
+
+    @Test fun `unknown cycle lane on right side`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
+            cycleway(null, UNKNOWN_LANE),
             parse(
-                "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "mandatory",
-                "cycleway:both:oneway" to "no",
-                "oneway" to "yes"
-            )
-        )
-
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
-            parse(
-                "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "exclusive_lane",
-                "cycleway:oneway" to "no",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, null),
-            parse(
-                "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "mandatory",
-                "cycleway:oneway" to "no",
-                "oneway" to "yes"
-            )
-        )
-    }
-
-    @Test fun `advisory lane on left side opposite`() {
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
-            parse(
-                "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "advisory",
-                "oneway" to "yes"
-            )
-        )
-    }
-
-    @Test fun `advisory lane synonyms on left side opposite`() {
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
-            parse(
-                "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "advisory_lane",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
-            parse(
-                "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "soft_lane",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, null),
-            parse(
-                "cycleway:left" to "opposite_lane",
-                "cycleway:left:lane" to "dashed",
-                "oneway" to "yes"
-            )
-        )
-    }
-
-    @Test fun `busway on left side opposite`() {
-        assertEquals(
-            LeftAndRightCycleway(BUSWAY, null),
-            parse(
-                "cycleway:left" to "opposite_share_busway",
-                "oneway" to "yes"
-            )
-        )
-    }
-
-    /* -------------------------------------- cycleway:right  ----------------------------------- */
-
-    @Test fun `unknown on right side`() {
-        assertEquals(
-            LeftAndRightCycleway(null, UNKNOWN),
-            parse("cycleway:right" to "something")
-        )
-    }
-
-    @Test fun `unknown cycle lane on right side`() {
-        assertEquals(
-            LeftAndRightCycleway(null, UNKNOWN_LANE),
-            parse(
-                "cycleway:right" to "lane",
-                "cycleway:right:lane" to "something"
+                "cycleway:right" to "lane",
+                "cycleway:right:lane" to "something"
             )
         )
     }
 
     @Test fun `unknown shared lane on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNKNOWN_SHARED_LANE),
+            cycleway(null, UNKNOWN_SHARED_LANE),
             parse(
                 "cycleway:right" to "shared_lane",
                 "cycleway:right:lane" to "something"
@@ -2009,21 +1798,21 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified shared lane on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNSPECIFIED_SHARED_LANE),
+            cycleway(null, UNSPECIFIED_SHARED_LANE),
             parse("cycleway:right" to "shared_lane")
         )
     }
 
     @Test fun `track right`() {
         assertEquals(
-            LeftAndRightCycleway(null, TRACK),
+            cycleway(null, TRACK),
             parse("cycleway:right" to "track")
         )
     }
 
     @Test fun `explicitly on sidewalk on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, SIDEWALK_EXPLICIT),
+            cycleway(null, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway:right" to "track",
                 "cycleway:right:segregated" to "no"
@@ -2031,14 +1820,14 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(null, SIDEWALK_EXPLICIT),
+            cycleway(null, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway:right" to "track",
                 "cycleway:both:segregated" to "no"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(null, SIDEWALK_EXPLICIT),
+            cycleway(null, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway:right" to "track",
                 "cycleway:segregated" to "no"
@@ -2048,21 +1837,21 @@ class CyclewayParserKtTest {
 
     @Test fun `dual track on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_TRACK),
+            cycleway(null, TRACK to BOTH),
             parse(
                 "cycleway:right" to "track",
                 "cycleway:right:oneway" to "no"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_TRACK),
+            cycleway(null, TRACK to BOTH),
             parse(
                 "cycleway:right" to "track",
                 "cycleway:both:oneway" to "no"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_TRACK),
+            cycleway(null, TRACK to BOTH),
             parse(
                 "cycleway:right" to "track",
                 "cycleway:oneway" to "no"
@@ -2072,14 +1861,14 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified lane on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNSPECIFIED_LANE),
+            cycleway(null, UNSPECIFIED_LANE),
             parse("cycleway:right" to "lane")
         )
     }
 
     @Test fun `unspecified dual lane on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway:right" to "lane",
                 "cycleway:right:oneway" to "no"
@@ -2087,14 +1876,14 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway:right" to "lane",
                 "cycleway:both:oneway" to "no"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway:right" to "lane",
                 "cycleway:oneway" to "no"
@@ -2104,7 +1893,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive lane on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, EXCLUSIVE_LANE),
+            cycleway(null, EXCLUSIVE_LANE),
             parse(
                 "cycleway:right" to "lane",
                 "cycleway:right:lane" to "exclusive"
@@ -2112,26 +1901,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive lane synonyms on right side`() {
-        assertEquals(
-            LeftAndRightCycleway(null, EXCLUSIVE_LANE),
-            parse(
-                "cycleway:right" to "lane",
-                "cycleway:right:lane" to "exclusive_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, EXCLUSIVE_LANE),
-            parse(
-                "cycleway:right" to "lane",
-                "cycleway:right:lane" to "mandatory"
-            )
-        )
-    }
-
     @Test fun `exclusive dual lane on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway:right" to "lane",
                 "cycleway:right:lane" to "exclusive",
@@ -2140,7 +1912,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway:right" to "lane",
                 "cycleway:right:lane" to "exclusive",
@@ -2148,7 +1920,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway:right" to "lane",
                 "cycleway:right:lane" to "exclusive",
@@ -2157,45 +1929,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive dual lane synonyms on right side`() {
-        assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
-            parse(
-                "cycleway:right" to "lane",
-                "cycleway:right:lane" to "exclusive_lane",
-                "cycleway:right:oneway" to "no"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
-            parse(
-                "cycleway:right" to "lane",
-                "cycleway:right:lane" to "mandatory",
-                "cycleway:right:oneway" to "no"
-            )
-        )
-
-        assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
-            parse(
-                "cycleway:right" to "lane",
-                "cycleway:right:lane" to "exclusive_lane",
-                "cycleway:both:oneway" to "no"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
-            parse(
-                "cycleway:right" to "lane",
-                "cycleway:right:lane" to "mandatory",
-                "cycleway:oneway" to "no"
-            )
-        )
-    }
-
     @Test fun `advisory lane on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
+            cycleway(null, ADVISORY_LANE),
             parse(
                 "cycleway:right" to "lane",
                 "cycleway:right:lane" to "advisory"
@@ -2203,33 +1939,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `advisory lane synonyms on right side`() {
-        assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
-            parse(
-                "cycleway:right" to "lane",
-                "cycleway:right:lane" to "advisory_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
-            parse(
-                "cycleway:right" to "lane",
-                "cycleway:right:lane" to "soft_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
-            parse(
-                "cycleway:right" to "lane",
-                "cycleway:right:lane" to "dashed"
-            )
-        )
-    }
-
     @Test fun `suggestion lane on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, SUGGESTION_LANE),
+            cycleway(null, SUGGESTION_LANE),
             parse(
                 "cycleway:right" to "shared_lane",
                 "cycleway:right:lane" to "advisory"
@@ -2237,33 +1949,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `suggestion lane synonyms on right side`() {
-        assertEquals(
-            LeftAndRightCycleway(null, SUGGESTION_LANE),
-            parse(
-                "cycleway:right" to "shared_lane",
-                "cycleway:right:lane" to "advisory_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, SUGGESTION_LANE),
-            parse(
-                "cycleway:right" to "shared_lane",
-                "cycleway:right:lane" to "soft_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, SUGGESTION_LANE),
-            parse(
-                "cycleway:right" to "shared_lane",
-                "cycleway:right:lane" to "dashed"
-            )
-        )
-    }
-
     @Test fun `pictograms on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, PICTOGRAMS),
+            cycleway(null, PICTOGRAMS),
             parse(
                 "cycleway:right" to "shared_lane",
                 "cycleway:right:lane" to "pictogram"
@@ -2273,32 +1961,28 @@ class CyclewayParserKtTest {
 
     @Test fun `none on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, NONE),
+            cycleway(null, NONE),
             parse("cycleway:right" to "no")
         )
-        assertEquals(
-            LeftAndRightCycleway(null, NONE),
-            parse("cycleway:right" to "none")
-        )
     }
 
     @Test fun `separate on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, SEPARATE),
+            cycleway(null, SEPARATE),
             parse("cycleway:right" to "separate")
         )
     }
 
     @Test fun `busway on right side`() {
         assertEquals(
-            LeftAndRightCycleway(null, BUSWAY),
+            cycleway(null, BUSWAY),
             parse("cycleway:right" to "share_busway")
         )
     }
 
     @Test fun `none on right side but oneway that isn't a oneway for cyclists`() {
         assertEquals(
-            LeftAndRightCycleway(NONE_NO_ONEWAY, NONE),
+            cycleway(NONE_NO_ONEWAY, NONE),
             parse(
                 "cycleway:right" to "no",
                 "oneway" to "yes",
@@ -2309,7 +1993,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none on right side but oneway that isn't a oneway for cyclists (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(null, NONE_NO_ONEWAY),
+            cycleway(null, NONE_NO_ONEWAY),
             parse(
                 "cycleway:right" to "no",
                 "oneway" to "-1",
@@ -2320,7 +2004,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none on right side but oneway that isn't a oneway for cyclists (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, NONE_NO_ONEWAY),
+            cycleway(null, NONE_NO_ONEWAY, true),
             parseForLeftHandTraffic(
                 "cycleway:right" to "no",
                 "oneway" to "yes",
@@ -2331,7 +2015,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none on right side but oneway that isn't a oneway for cyclists (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE_NO_ONEWAY, NONE),
+            cycleway(NONE_NO_ONEWAY, NONE, true),
             parseForLeftHandTraffic(
                 "cycleway:right" to "no",
                 "oneway" to "-1",
@@ -2340,9 +2024,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `track on left side that is not in contraflow direction is unsupported`() {
+    @Test fun `track on left side that is not in contraflow direction`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN, NONE),
+            cycleway(TRACK to FORWARD, NONE to FORWARD),
             parse(
                 "cycleway:right" to "no",
                 "cycleway:left" to "track",
@@ -2351,9 +2035,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `track on left side for left-hand-traffic that is not in flow direction is unsupported`() {
+    @Test fun `track on left side for left-hand-traffic that is not in flow direction`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN, NONE),
+            cycleway(TRACK to BACKWARD, NONE to BACKWARD),
             parseForLeftHandTraffic(
                 "cycleway:right" to "no",
                 "cycleway:left" to "track",
@@ -2362,9 +2046,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `track on right side that is not in flow direction is unsupported`() {
+    @Test fun `track on right side that is not in flow direction`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, UNKNOWN),
+            cycleway(NONE to BACKWARD, TRACK to BACKWARD),
             parse(
                 "cycleway:left" to "no",
                 "cycleway:right" to "track",
@@ -2373,9 +2057,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `track on right side for left-hand-traffic that is not in contraflow direction is unsupported`() {
+    @Test fun `track on right side for left-hand-traffic that is not in contraflow direction`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, UNKNOWN),
+            cycleway(NONE to FORWARD, TRACK to FORWARD),
             parseForLeftHandTraffic(
                 "cycleway:left" to "no",
                 "cycleway:right" to "track",
@@ -2384,11 +2068,18 @@ class CyclewayParserKtTest {
         )
     }
 
+    @Test fun `shoulder on right side`() {
+        assertEquals(
+            cycleway(null, SHOULDER),
+            parse("cycleway:right" to "shoulder")
+        )
+    }
+
     /* ------------------------------ cycleway:right opposite tagging --------------------------- */
 
     @Test fun `right opposite`() {
         assertEquals(
-            LeftAndRightCycleway(null, NONE),
+            cycleway(null, NONE),
             parse(
                 "cycleway:right" to "opposite",
                 "oneway" to "yes"
@@ -2398,7 +2089,7 @@ class CyclewayParserKtTest {
 
     @Test fun `right opposite (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(null, NONE_NO_ONEWAY),
+            cycleway(null, NONE_NO_ONEWAY, true),
             parseForLeftHandTraffic(
                 "cycleway:right" to "opposite",
                 "oneway" to "yes"
@@ -2408,7 +2099,7 @@ class CyclewayParserKtTest {
 
     @Test fun `track right opposite`() {
         assertEquals(
-            LeftAndRightCycleway(null, TRACK),
+            cycleway(null, TRACK),
             parse(
                 "cycleway:right" to "opposite_track",
                 "oneway" to "yes"
@@ -2418,7 +2109,7 @@ class CyclewayParserKtTest {
 
     @Test fun `explicitly on sidewalk on right side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(null, SIDEWALK_EXPLICIT),
+            cycleway(null, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway:right" to "opposite_track",
                 "cycleway:right:segregated" to "no",
@@ -2427,7 +2118,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(null, SIDEWALK_EXPLICIT),
+            cycleway(null, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway:right" to "opposite_track",
                 "cycleway:both:segregated" to "no",
@@ -2435,7 +2126,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(null, SIDEWALK_EXPLICIT),
+            cycleway(null, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway:right" to "opposite_track",
                 "cycleway:segregated" to "no",
@@ -2446,7 +2137,7 @@ class CyclewayParserKtTest {
 
     @Test fun `dual track on right side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_TRACK),
+            cycleway(null, TRACK to BOTH),
             parse(
                 "cycleway:right" to "opposite_track",
                 "cycleway:right:oneway" to "no",
@@ -2455,7 +2146,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_TRACK),
+            cycleway(null, TRACK to BOTH),
             parse(
                 "cycleway:right" to "opposite_track",
                 "cycleway:both:oneway" to "no",
@@ -2463,7 +2154,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_TRACK),
+            cycleway(null, TRACK to BOTH),
             parse(
                 "cycleway:right" to "opposite_track",
                 "cycleway:oneway" to "no",
@@ -2474,7 +2165,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified lane on right side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(null, UNSPECIFIED_LANE),
+            cycleway(null, UNSPECIFIED_LANE),
             parse(
                 "cycleway:right" to "opposite_lane",
                 "oneway" to "yes"
@@ -2484,7 +2175,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified dual lane on right side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway:right" to "opposite_lane",
                 "cycleway:right:oneway" to "no",
@@ -2493,7 +2184,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway:right" to "opposite_lane",
                 "cycleway:both:oneway" to "no",
@@ -2501,7 +2192,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway:right" to "opposite_lane",
                 "cycleway:oneway" to "no",
@@ -2512,7 +2203,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive lane on right side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(null, EXCLUSIVE_LANE),
+            cycleway(null, EXCLUSIVE_LANE),
             parse(
                 "cycleway:right" to "opposite_lane",
                 "cycleway:right:lane" to "exclusive",
@@ -2521,28 +2212,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive lane synonyms on right side opposite`() {
-        assertEquals(
-            LeftAndRightCycleway(null, EXCLUSIVE_LANE),
-            parse(
-                "cycleway:right" to "opposite_lane",
-                "cycleway:right:lane" to "exclusive_lane",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, EXCLUSIVE_LANE),
-            parse(
-                "cycleway:right" to "opposite_lane",
-                "cycleway:right:lane" to "mandatory",
-                "oneway" to "yes"
-            )
-        )
-    }
-
     @Test fun `exclusive dual lane on right side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway:right" to "opposite_lane",
                 "cycleway:right:lane" to "exclusive",
@@ -2552,7 +2224,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway:right" to "opposite_lane",
                 "cycleway:right:lane" to "exclusive",
@@ -2561,7 +2233,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
+            cycleway(null, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway:right" to "opposite_lane",
                 "cycleway:right:lane" to "exclusive",
@@ -2571,49 +2243,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive dual lane synonyms on right side opposite`() {
-        assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
-            parse(
-                "cycleway:right" to "opposite_lane",
-                "cycleway:right:lane" to "exclusive_lane",
-                "cycleway:right:oneway" to "no",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
-            parse(
-                "cycleway:right" to "opposite_lane",
-                "cycleway:right:lane" to "mandatory",
-                "cycleway:right:oneway" to "no",
-                "oneway" to "yes"
-            )
-        )
-
-        assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
-            parse(
-                "cycleway:right" to "opposite_lane",
-                "cycleway:right:lane" to "exclusive_lane",
-                "cycleway:both:oneway" to "no",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, DUAL_LANE),
-            parse(
-                "cycleway:right" to "opposite_lane",
-                "cycleway:right:lane" to "mandatory",
-                "cycleway:oneway" to "no",
-                "oneway" to "yes"
-            )
-        )
-    }
-
     @Test fun `advisory lane on right side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
+            cycleway(null, ADVISORY_LANE),
             parse(
                 "cycleway:right" to "opposite_lane",
                 "cycleway:right:lane" to "advisory",
@@ -2622,36 +2254,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `advisory lane synonyms on right side opposite`() {
-        assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
-            parse(
-                "cycleway:right" to "opposite_lane",
-                "cycleway:right:lane" to "advisory_lane",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
-            parse(
-                "cycleway:right" to "opposite_lane",
-                "cycleway:right:lane" to "soft_lane",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(null, ADVISORY_LANE),
-            parse(
-                "cycleway:right" to "opposite_lane",
-                "cycleway:right:lane" to "dashed",
-                "oneway" to "yes"
-            )
-        )
-    }
-
     @Test fun `busway on right side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(null, BUSWAY),
+            cycleway(null, BUSWAY),
             parse(
                 "cycleway:right" to "opposite_share_busway",
                 "oneway" to "yes"
@@ -2663,14 +2268,14 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN, UNKNOWN),
+            cycleway(UNKNOWN, UNKNOWN),
             parse("cycleway:both" to "something")
         )
     }
 
     @Test fun `unknown cycle lane on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN_LANE, UNKNOWN_LANE),
+            cycleway(UNKNOWN_LANE, UNKNOWN_LANE),
             parse(
                 "cycleway:both" to "lane",
                 "cycleway:both:lane" to "something"
@@ -2680,7 +2285,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unknown shared lane on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(UNKNOWN_SHARED_LANE, UNKNOWN_SHARED_LANE),
+            cycleway(UNKNOWN_SHARED_LANE, UNKNOWN_SHARED_LANE),
             parse(
                 "cycleway:both" to "shared_lane",
                 "cycleway:both:lane" to "something"
@@ -2690,28 +2295,28 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified shared lane on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_SHARED_LANE, UNSPECIFIED_SHARED_LANE),
+            cycleway(UNSPECIFIED_SHARED_LANE, UNSPECIFIED_SHARED_LANE),
             parse("cycleway:both" to "shared_lane")
         )
     }
 
     @Test fun `track on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(TRACK, TRACK),
+            cycleway(TRACK, TRACK),
             parse("cycleway:both" to "track")
         )
     }
 
     @Test fun `explicitly on sidewalk on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
+            cycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway:both" to "track",
                 "cycleway:both:segregated" to "no"
             )
         )
         assertEquals(
-            LeftAndRightCycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
+            cycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway:both" to "track",
                 "cycleway:segregated" to "no"
@@ -2721,7 +2326,7 @@ class CyclewayParserKtTest {
 
     @Test fun `dual track on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, DUAL_TRACK),
+            cycleway(TRACK to BOTH, TRACK to BOTH),
             parse(
                 "cycleway:both" to "track",
                 "cycleway:both:oneway" to "no"
@@ -2729,7 +2334,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, DUAL_TRACK),
+            cycleway(TRACK to BOTH, TRACK to BOTH),
             parse(
                 "cycleway:both" to "track",
                 "cycleway:oneway" to "no"
@@ -2739,14 +2344,14 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified lane on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_LANE, UNSPECIFIED_LANE),
+            cycleway(UNSPECIFIED_LANE, UNSPECIFIED_LANE),
             parse("cycleway:both" to "lane")
         )
     }
 
     @Test fun `unspecified dual lane on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
+            cycleway(UNSPECIFIED_LANE to BOTH, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway:both" to "lane",
                 "cycleway:both:oneway" to "no"
@@ -2754,7 +2359,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
+            cycleway(UNSPECIFIED_LANE to BOTH, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway:both" to "lane",
                 "cycleway:oneway" to "no"
@@ -2764,7 +2369,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive lane on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
+            cycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
             parse(
                 "cycleway:both" to "lane",
                 "cycleway:both:lane" to "exclusive"
@@ -2772,26 +2377,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive lane synonyms on both sides`() {
-        assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
-            parse(
-                "cycleway:both" to "lane",
-                "cycleway:both:lane" to "exclusive_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
-            parse(
-                "cycleway:both" to "lane",
-                "cycleway:both:lane" to "mandatory"
-            )
-        )
-    }
-
     @Test fun `exclusive dual lane on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
+            cycleway(EXCLUSIVE_LANE to BOTH, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway:both" to "lane",
                 "cycleway:both:lane" to "exclusive",
@@ -2800,7 +2388,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
+            cycleway(EXCLUSIVE_LANE to BOTH, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway:both" to "lane",
                 "cycleway:both:lane" to "exclusive",
@@ -2809,45 +2397,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive dual lane synonyms on both sides`() {
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
-            parse(
-                "cycleway:both" to "lane",
-                "cycleway:both:lane" to "exclusive_lane",
-                "cycleway:both:oneway" to "no"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
-            parse(
-                "cycleway:both" to "lane",
-                "cycleway:both:lane" to "mandatory",
-                "cycleway:both:oneway" to "no"
-            )
-        )
-
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
-            parse(
-                "cycleway:both" to "lane",
-                "cycleway:both:lane" to "exclusive_lane",
-                "cycleway:oneway" to "no"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
-            parse(
-                "cycleway:both" to "lane",
-                "cycleway:both:lane" to "mandatory",
-                "cycleway:oneway" to "no"
-            )
-        )
-    }
-
     @Test fun `advisory lane on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
+            cycleway(ADVISORY_LANE, ADVISORY_LANE),
             parse(
                 "cycleway:both" to "lane",
                 "cycleway:both:lane" to "advisory"
@@ -2855,33 +2407,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `advisory lane synonyms on both sides`() {
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
-            parse(
-                "cycleway:both" to "lane",
-                "cycleway:both:lane" to "advisory_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
-            parse(
-                "cycleway:both" to "lane",
-                "cycleway:both:lane" to "soft_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
-            parse(
-                "cycleway:both" to "lane",
-                "cycleway:both:lane" to "dashed"
-            )
-        )
-    }
-
     @Test fun `suggestion lane on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, SUGGESTION_LANE),
+            cycleway(SUGGESTION_LANE, SUGGESTION_LANE),
             parse(
                 "cycleway:both" to "shared_lane",
                 "cycleway:both:lane" to "advisory"
@@ -2889,33 +2417,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `suggestion lane synonyms on both sides`() {
-        assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, SUGGESTION_LANE),
-            parse(
-                "cycleway:both" to "shared_lane",
-                "cycleway:both:lane" to "advisory_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, SUGGESTION_LANE),
-            parse(
-                "cycleway:both" to "shared_lane",
-                "cycleway:both:lane" to "soft_lane"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(SUGGESTION_LANE, SUGGESTION_LANE),
-            parse(
-                "cycleway:both" to "shared_lane",
-                "cycleway:both:lane" to "dashed"
-            )
-        )
-    }
-
     @Test fun `pictograms on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(PICTOGRAMS, PICTOGRAMS),
+            cycleway(PICTOGRAMS, PICTOGRAMS),
             parse(
                 "cycleway:both" to "shared_lane",
                 "cycleway:both:lane" to "pictogram"
@@ -2925,32 +2429,28 @@ class CyclewayParserKtTest {
 
     @Test fun `none on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, NONE),
+            cycleway(NONE, NONE),
             parse("cycleway:both" to "no")
         )
-        assertEquals(
-            LeftAndRightCycleway(NONE, NONE),
-            parse("cycleway:both" to "none")
-        )
     }
 
     @Test fun `separate on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(SEPARATE, SEPARATE),
+            cycleway(SEPARATE, SEPARATE),
             parse("cycleway:both" to "separate")
         )
     }
 
     @Test fun `busway on both sides`() {
         assertEquals(
-            LeftAndRightCycleway(BUSWAY, BUSWAY),
+            cycleway(BUSWAY, BUSWAY),
             parse("cycleway:both" to "share_busway")
         )
     }
 
     @Test fun `none on both sides but oneway that isn't a oneway for cyclists`() {
         assertEquals(
-            LeftAndRightCycleway(NONE_NO_ONEWAY, NONE),
+            cycleway(NONE_NO_ONEWAY, NONE),
             parse(
                 "cycleway:both" to "no",
                 "oneway" to "yes",
@@ -2961,7 +2461,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none on both sides but oneway that isn't a oneway for cyclists (reversed)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, NONE_NO_ONEWAY),
+            cycleway(NONE, NONE_NO_ONEWAY),
             parse(
                 "cycleway:both" to "no",
                 "oneway" to "-1",
@@ -2972,7 +2472,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none on both sides but oneway that isn't a oneway for cyclists (left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE, NONE_NO_ONEWAY),
+            cycleway(NONE, NONE_NO_ONEWAY, true),
             parseForLeftHandTraffic(
                 "cycleway:both" to "no",
                 "oneway" to "yes",
@@ -2983,7 +2483,7 @@ class CyclewayParserKtTest {
 
     @Test fun `none on both sides but oneway that isn't a oneway for cyclists (reversed + left hand traffic)`() {
         assertEquals(
-            LeftAndRightCycleway(NONE_NO_ONEWAY, NONE),
+            cycleway(NONE_NO_ONEWAY, NONE, true),
             parseForLeftHandTraffic(
                 "cycleway:both" to "no",
                 "oneway" to "-1",
@@ -2992,11 +2492,18 @@ class CyclewayParserKtTest {
         )
     }
 
+    @Test fun `shoulder on both sides`() {
+        assertEquals(
+            cycleway(SHOULDER, SHOULDER),
+            parse("cycleway:both" to "shoulder")
+        )
+    }
+
     /* ------------------------------ cycleway:both opposite tagging --------------------------- */
 
     @Test fun `track both opposite`() {
         assertEquals(
-            LeftAndRightCycleway(TRACK, TRACK),
+            cycleway(TRACK, TRACK),
             parse(
                 "cycleway:both" to "opposite_track",
                 "oneway" to "yes"
@@ -3006,7 +2513,7 @@ class CyclewayParserKtTest {
 
     @Test fun `explicitly on sidewalk on both side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
+            cycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway:both" to "opposite_track",
                 "cycleway:both:segregated" to "no",
@@ -3015,7 +2522,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
+            cycleway(SIDEWALK_EXPLICIT, SIDEWALK_EXPLICIT),
             parse(
                 "cycleway:both" to "opposite_track",
                 "cycleway:segregated" to "no",
@@ -3026,7 +2533,7 @@ class CyclewayParserKtTest {
 
     @Test fun `dual track on both side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, DUAL_TRACK),
+            cycleway(TRACK to BOTH, TRACK to BOTH),
             parse(
                 "cycleway:both" to "opposite_track",
                 "cycleway:both:oneway" to "no",
@@ -3035,7 +2542,7 @@ class CyclewayParserKtTest {
         )
 
         assertEquals(
-            LeftAndRightCycleway(DUAL_TRACK, DUAL_TRACK),
+            cycleway(TRACK to BOTH, TRACK to BOTH),
             parse(
                 "cycleway:both" to "opposite_track",
                 "cycleway:oneway" to "no",
@@ -3046,7 +2553,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified lane on both side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(UNSPECIFIED_LANE, UNSPECIFIED_LANE),
+            cycleway(UNSPECIFIED_LANE, UNSPECIFIED_LANE),
             parse(
                 "cycleway:both" to "opposite_lane",
                 "oneway" to "yes"
@@ -3056,7 +2563,7 @@ class CyclewayParserKtTest {
 
     @Test fun `unspecified dual lane on both side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
+            cycleway(UNSPECIFIED_LANE to BOTH, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway:both" to "opposite_lane",
                 "cycleway:both:oneway" to "no",
@@ -3064,7 +2571,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
+            cycleway(UNSPECIFIED_LANE to BOTH, UNSPECIFIED_LANE to BOTH),
             parse(
                 "cycleway:both" to "opposite_lane",
                 "cycleway:oneway" to "no",
@@ -3075,7 +2582,7 @@ class CyclewayParserKtTest {
 
     @Test fun `exclusive lane on both side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
+            cycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
             parse(
                 "cycleway:both" to "opposite_lane",
                 "cycleway:both:lane" to "exclusive",
@@ -3084,28 +2591,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive lane synonyms on both side opposite`() {
-        assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
-            parse(
-                "cycleway:both" to "opposite_lane",
-                "cycleway:both:lane" to "exclusive_lane",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(EXCLUSIVE_LANE, EXCLUSIVE_LANE),
-            parse(
-                "cycleway:both" to "opposite_lane",
-                "cycleway:both:lane" to "mandatory",
-                "oneway" to "yes"
-            )
-        )
-    }
-
     @Test fun `exclusive dual lane on both side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
+            cycleway(EXCLUSIVE_LANE to BOTH, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway:both" to "opposite_lane",
                 "cycleway:both:lane" to "exclusive",
@@ -3114,7 +2602,7 @@ class CyclewayParserKtTest {
             )
         )
         assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
+            cycleway(EXCLUSIVE_LANE to BOTH, EXCLUSIVE_LANE to BOTH),
             parse(
                 "cycleway:both" to "opposite_lane",
                 "cycleway:both:lane" to "exclusive",
@@ -3124,49 +2612,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `exclusive dual lane synonyms on both side opposite`() {
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
-            parse(
-                "cycleway:both" to "opposite_lane",
-                "cycleway:both:lane" to "exclusive_lane",
-                "cycleway:both:oneway" to "no",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
-            parse(
-                "cycleway:both" to "opposite_lane",
-                "cycleway:both:lane" to "mandatory",
-                "cycleway:both:oneway" to "no",
-                "oneway" to "yes"
-            )
-        )
-
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
-            parse(
-                "cycleway:both" to "opposite_lane",
-                "cycleway:both:lane" to "exclusive_lane",
-                "cycleway:oneway" to "no",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(DUAL_LANE, DUAL_LANE),
-            parse(
-                "cycleway:both" to "opposite_lane",
-                "cycleway:both:lane" to "mandatory",
-                "cycleway:oneway" to "no",
-                "oneway" to "yes"
-            )
-        )
-    }
-
     @Test fun `advisory lane on both side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
+            cycleway(ADVISORY_LANE, ADVISORY_LANE),
             parse(
                 "cycleway:both" to "opposite_lane",
                 "cycleway:both:lane" to "advisory",
@@ -3175,36 +2623,9 @@ class CyclewayParserKtTest {
         )
     }
 
-    @Test fun `advisory lane synonyms on both side opposite`() {
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
-            parse(
-                "cycleway:both" to "opposite_lane",
-                "cycleway:both:lane" to "advisory_lane",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
-            parse(
-                "cycleway:both" to "opposite_lane",
-                "cycleway:both:lane" to "soft_lane",
-                "oneway" to "yes"
-            )
-        )
-        assertEquals(
-            LeftAndRightCycleway(ADVISORY_LANE, ADVISORY_LANE),
-            parse(
-                "cycleway:both" to "opposite_lane",
-                "cycleway:both:lane" to "dashed",
-                "oneway" to "yes"
-            )
-        )
-    }
-
     @Test fun `busway on both side opposite`() {
         assertEquals(
-            LeftAndRightCycleway(BUSWAY, BUSWAY),
+            cycleway(BUSWAY, BUSWAY),
             parse(
                 "cycleway:both" to "opposite_share_busway",
                 "oneway" to "yes"
@@ -3232,6 +2653,18 @@ class CyclewayParserKtTest {
     }
 }
 
+private fun cycleway(left: Pair<Cycleway, Direction>?, right: Pair<Cycleway, Direction>?) =
+    LeftAndRightCycleway(
+        left?.let { CyclewayAndDirection(it.first, it.second) },
+        right?.let { CyclewayAndDirection(it.first, it.second) },
+    )
+
+private fun cycleway(left: Cycleway?, right: Cycleway?, isLeftHandTraffic: Boolean = false) =
+    LeftAndRightCycleway(
+        left?.let { CyclewayAndDirection(it, if (isLeftHandTraffic) FORWARD else BACKWARD) },
+        right?.let { CyclewayAndDirection(it, if (isLeftHandTraffic) BACKWARD else FORWARD) },
+    )
+
 private fun parse(vararg pairs: Pair<String, String>) =
     createCyclewaySides(mapOf(*pairs), false)
 
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayCreatorKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayCreatorKtTest.kt
new file mode 100644
index 00000000000..4c776c379de
--- /dev/null
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayCreatorKtTest.kt
@@ -0,0 +1,576 @@
+package de.westnordost.streetcomplete.osm.cycleway_separate
+
+import de.westnordost.streetcomplete.osm.cycleway_separate.SeparateCycleway.*
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
+import org.assertj.core.api.Assertions
+import org.junit.Test
+
+class SeparateCyclewayCreatorKtTest {
+    @Test fun `apply none`() {
+        verifyAnswer(
+            mapOf("highway" to "footway"),
+            NONE,
+            arrayOf(StringMapEntryAdd("bicycle", "no"))
+        )
+    }
+
+    @Test fun `apply none re-tags cycleway and adds foot=yes if foot was not yes before`() {
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "foot" to "no"),
+            NONE,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "no"),
+                StringMapEntryModify("highway", "cycleway", "path"),
+                StringMapEntryModify("foot", "no", "yes")
+            )
+        )
+    }
+
+    @Test fun `apply none re-tags cycleway to footway if foot is designated`() {
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "foot" to "designated"),
+            NONE,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "no"),
+                StringMapEntryModify("highway", "cycleway", "footway")
+            )
+        )
+    }
+
+    @Test fun `apply none removes sidewalk and segregated tags`() {
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "segregated" to "yes",
+                "sidewalk" to "both",
+            ),
+            NONE,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "no"),
+                StringMapEntryDelete("segregated", "yes"),
+                StringMapEntryDelete("sidewalk", "both"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "segregated" to "yes",
+                "sidewalk:both" to "yes",
+            ),
+            NONE,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "no"),
+                StringMapEntryDelete("segregated", "yes"),
+                StringMapEntryDelete("sidewalk:both", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "segregated" to "yes",
+                "sidewalk:left" to "yes",
+                "sidewalk:right" to "yes",
+            ),
+            NONE,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "no"),
+                StringMapEntryDelete("segregated", "yes"),
+                StringMapEntryDelete("sidewalk:left", "yes"),
+                StringMapEntryDelete("sidewalk:right", "yes"),
+            )
+        )
+    }
+
+    @Test fun `apply allowed`() {
+        verifyAnswer(
+            mapOf("highway" to "footway"),
+            ALLOWED,
+            arrayOf(StringMapEntryAdd("bicycle", "yes"))
+        )
+    }
+
+    @Test fun `apply allowed re-tags cycleway and adds foot=yes if foot was not yes before`() {
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "foot" to "no"),
+            ALLOWED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "yes"),
+                StringMapEntryModify("highway", "cycleway", "path"),
+                StringMapEntryModify("foot", "no", "yes")
+            )
+        )
+    }
+
+    @Test fun `apply allowed re-tags cycleway to footway if foot is designated`() {
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "foot" to "designated"),
+            ALLOWED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "yes"),
+                StringMapEntryModify("highway", "cycleway", "footway")
+            )
+        )
+    }
+
+    @Test fun `apply allowed removes sidewalk and segregated tags`() {
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "segregated" to "yes",
+                "sidewalk" to "both",
+            ),
+            ALLOWED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "yes"),
+                StringMapEntryDelete("segregated", "yes"),
+                StringMapEntryDelete("sidewalk", "both"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "segregated" to "yes",
+                "sidewalk:both" to "yes",
+            ),
+            ALLOWED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "yes"),
+                StringMapEntryDelete("segregated", "yes"),
+                StringMapEntryDelete("sidewalk:both", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "segregated" to "yes",
+                "sidewalk:left" to "yes",
+                "sidewalk:right" to "yes",
+            ),
+            ALLOWED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "yes"),
+                StringMapEntryDelete("segregated", "yes"),
+                StringMapEntryDelete("sidewalk:left", "yes"),
+                StringMapEntryDelete("sidewalk:right", "yes"),
+            )
+        )
+    }
+
+    @Test fun `apply non-designated`() {
+        verifyAnswer(
+            mapOf("highway" to "footway"),
+            NON_DESIGNATED,
+            arrayOf(StringMapEntryAdd("check_date:bicycle", nowAsCheckDateString()))
+        )
+    }
+
+    @Test fun `apply non-designated does not change bicycle tag unless it is designated`() {
+        verifyAnswer(
+            mapOf("highway" to "footway", "bicycle" to "designated"),
+            NON_DESIGNATED,
+            arrayOf(StringMapEntryDelete("bicycle", "designated"))
+        )
+        verifyAnswer(
+            mapOf("highway" to "footway", "bicycle" to "yes"),
+            NON_DESIGNATED,
+            arrayOf(StringMapEntryAdd("check_date:bicycle", nowAsCheckDateString()))
+        )
+        verifyAnswer(
+            mapOf("highway" to "footway", "bicycle" to "no"),
+            NON_DESIGNATED,
+            arrayOf(StringMapEntryAdd("check_date:bicycle", nowAsCheckDateString()))
+        )
+    }
+
+    @Test fun `apply non-designated re-tags cycleway and adds foot=yes if foot was not yes before`() {
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "foot" to "no"),
+            NON_DESIGNATED,
+            arrayOf(
+                StringMapEntryModify("highway", "cycleway", "path"),
+                StringMapEntryModify("foot", "no", "yes")
+            )
+        )
+    }
+
+    @Test fun `apply non-designated re-tags cycleway to footway if foot is designated`() {
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "foot" to "designated"),
+            NON_DESIGNATED,
+            arrayOf(
+                StringMapEntryModify("highway", "cycleway", "footway")
+            )
+        )
+    }
+
+    @Test fun `apply non-designated removes sidewalk and segregated tags`() {
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "segregated" to "yes",
+                "sidewalk" to "both",
+            ),
+            NON_DESIGNATED,
+            arrayOf(
+                StringMapEntryDelete("segregated", "yes"),
+                StringMapEntryDelete("sidewalk", "both"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "segregated" to "yes",
+                "sidewalk:left" to "yes",
+                "sidewalk:right" to "yes",
+            ),
+            NON_DESIGNATED,
+            arrayOf(
+                StringMapEntryDelete("segregated", "yes"),
+                StringMapEntryDelete("sidewalk:left", "yes"),
+                StringMapEntryDelete("sidewalk:right", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "segregated" to "yes",
+                "sidewalk:both" to "yes",
+            ),
+            NON_DESIGNATED,
+            arrayOf(
+                StringMapEntryDelete("segregated", "yes"),
+                StringMapEntryDelete("sidewalk:both", "yes"),
+            )
+        )
+    }
+
+    @Test fun `apply non-segregated`() {
+        verifyAnswer(
+            mapOf("highway" to "footway"),
+            NON_SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "designated"),
+                StringMapEntryAdd("segregated", "no"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "footway", "foot" to "yes"),
+            NON_SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "designated"),
+                StringMapEntryModify("foot", "yes", "designated"),
+                StringMapEntryAdd("segregated", "no"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "cycleway"),
+            NON_SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("foot", "designated"),
+                StringMapEntryAdd("segregated", "no"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "bicycle" to "yes"),
+            NON_SEGREGATED,
+            arrayOf(
+                StringMapEntryModify("bicycle", "yes", "designated"),
+                StringMapEntryAdd("foot", "designated"),
+                StringMapEntryAdd("segregated", "no"),
+            )
+        )
+    }
+
+    @Test fun `apply non-segregated removes sidewalk tags`() {
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "sidewalk" to "both",
+            ),
+            NON_SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "designated"),
+                StringMapEntryAdd("segregated", "no"),
+                StringMapEntryDelete("sidewalk", "both"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "sidewalk:both" to "yes",
+            ),
+            NON_SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "designated"),
+                StringMapEntryAdd("segregated", "no"),
+                StringMapEntryDelete("sidewalk:both", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "sidewalk:left" to "yes",
+                "sidewalk:right" to "yes",
+            ),
+            NON_SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "designated"),
+                StringMapEntryAdd("segregated", "no"),
+                StringMapEntryDelete("sidewalk:left", "yes"),
+                StringMapEntryDelete("sidewalk:right", "yes"),
+            )
+        )
+    }
+
+    @Test fun `apply segregated`() {
+        verifyAnswer(
+            mapOf("highway" to "footway"),
+            SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "designated"),
+                StringMapEntryAdd("segregated", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "footway", "foot" to "yes"),
+            SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "designated"),
+                StringMapEntryModify("foot", "yes", "designated"),
+                StringMapEntryAdd("segregated", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "cycleway"),
+            SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("foot", "designated"),
+                StringMapEntryAdd("segregated", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "bicycle" to "yes"),
+            SEGREGATED,
+            arrayOf(
+                StringMapEntryModify("bicycle", "yes", "designated"),
+                StringMapEntryAdd("foot", "designated"),
+                StringMapEntryAdd("segregated", "yes"),
+            )
+        )
+    }
+
+    @Test fun `apply segregated removes sidewalk tags`() {
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "sidewalk" to "both",
+            ),
+            SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "designated"),
+                StringMapEntryAdd("segregated", "yes"),
+                StringMapEntryDelete("sidewalk", "both"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "sidewalk:both" to "yes",
+            ),
+            SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "designated"),
+                StringMapEntryAdd("segregated", "yes"),
+                StringMapEntryDelete("sidewalk:both", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "footway",
+                "sidewalk:left" to "yes",
+                "sidewalk:right" to "yes",
+            ),
+            SEGREGATED,
+            arrayOf(
+                StringMapEntryAdd("bicycle", "designated"),
+                StringMapEntryAdd("segregated", "yes"),
+                StringMapEntryDelete("sidewalk:left", "yes"),
+                StringMapEntryDelete("sidewalk:right", "yes"),
+            )
+        )
+    }
+
+    @Test fun `apply exclusive`() {
+        verifyAnswer(
+            mapOf("highway" to "cycleway"),
+            EXCLUSIVE,
+            arrayOf(
+                StringMapEntryAdd("foot", "no"),
+                StringMapEntryModify("highway", "cycleway", "cycleway"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "footway"),
+            EXCLUSIVE,
+            arrayOf(
+                StringMapEntryAdd("foot", "no"),
+                StringMapEntryModify("highway", "footway", "cycleway"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "path", "foot" to "yes"),
+            EXCLUSIVE,
+            arrayOf(
+                StringMapEntryModify("foot", "yes", "no"),
+                StringMapEntryModify("highway", "path", "cycleway"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "bicycle" to "yes"),
+            EXCLUSIVE,
+            arrayOf(
+                StringMapEntryAdd("foot", "no"),
+                StringMapEntryModify("bicycle", "yes", "designated"),
+                StringMapEntryModify("highway", "cycleway", "cycleway"),
+            )
+        )
+    }
+
+    @Test fun `apply exclusive removes sidewalk and segregated tags`() {
+        verifyAnswer(
+            mapOf(
+                "highway" to "cycleway",
+                "sidewalk" to "both",
+                "segregated" to "yes",
+            ),
+            EXCLUSIVE,
+            arrayOf(
+                StringMapEntryAdd("foot", "no"),
+                StringMapEntryModify("highway", "cycleway", "cycleway"),
+                StringMapEntryDelete("sidewalk", "both"),
+                StringMapEntryDelete("segregated", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "cycleway",
+                "segregated" to "yes",
+                "sidewalk:both" to "yes",
+            ),
+            EXCLUSIVE,
+            arrayOf(
+                StringMapEntryAdd("foot", "no"),
+                StringMapEntryModify("highway", "cycleway", "cycleway"),
+                StringMapEntryDelete("segregated", "yes"),
+                StringMapEntryDelete("sidewalk:both", "yes"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "highway" to "cycleway",
+                "segregated" to "yes",
+                "sidewalk:left" to "yes",
+                "sidewalk:right" to "yes",
+            ),
+            EXCLUSIVE,
+            arrayOf(
+                StringMapEntryAdd("foot", "no"),
+                StringMapEntryModify("highway", "cycleway", "cycleway"),
+                StringMapEntryDelete("segregated", "yes"),
+                StringMapEntryDelete("sidewalk:left", "yes"),
+                StringMapEntryDelete("sidewalk:right", "yes"),
+            )
+        )
+    }
+
+    @Test fun `apply exclusive with sidewalk`() {
+        verifyAnswer(
+            mapOf("highway" to "cycleway"),
+            EXCLUSIVE_WITH_SIDEWALK,
+            arrayOf(
+                StringMapEntryAdd("sidewalk", "yes"),
+                StringMapEntryModify("highway", "cycleway", "cycleway"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "foot" to "yes"),
+            EXCLUSIVE_WITH_SIDEWALK,
+            arrayOf(
+                StringMapEntryAdd("sidewalk", "yes"),
+                StringMapEntryDelete("foot", "yes"),
+                StringMapEntryModify("highway", "cycleway", "cycleway"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "footway"),
+            EXCLUSIVE_WITH_SIDEWALK,
+            arrayOf(
+                StringMapEntryAdd("sidewalk", "yes"),
+                StringMapEntryModify("highway", "footway", "cycleway"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "path"),
+            EXCLUSIVE_WITH_SIDEWALK,
+            arrayOf(
+                StringMapEntryAdd("sidewalk", "yes"),
+                StringMapEntryModify("highway", "path", "cycleway"),
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "path", "bicycle" to "yes"),
+            EXCLUSIVE_WITH_SIDEWALK,
+            arrayOf(
+                StringMapEntryAdd("sidewalk", "yes"),
+                StringMapEntryModify("bicycle", "yes", "designated"),
+                StringMapEntryModify("highway", "path", "cycleway"),
+            )
+        )
+    }
+
+    @Test fun `apply exclusive with sidewalk does not touch sidewalk tags if a sidewalk is already tagged`() {
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "sidewalk" to "both"),
+            EXCLUSIVE_WITH_SIDEWALK,
+            arrayOf(
+                StringMapEntryModify("highway", "cycleway", "cycleway"),
+                StringMapEntryAdd("check_date:bicycle", nowAsCheckDateString())
+            )
+        )
+        verifyAnswer(
+            mapOf("highway" to "cycleway", "sidewalk:left" to "yes"),
+            EXCLUSIVE_WITH_SIDEWALK,
+            arrayOf(
+                StringMapEntryModify("highway", "cycleway", "cycleway"),
+                StringMapEntryAdd("check_date:bicycle", nowAsCheckDateString())
+            )
+        )
+    }
+
+    @Test fun `apply exclusive with sidewalk removes segregated tags`() {
+        verifyAnswer(
+            mapOf(
+                "highway" to "cycleway",
+                "sidewalk" to "both",
+                "segregated" to "yes"
+            ),
+            EXCLUSIVE_WITH_SIDEWALK,
+            arrayOf(
+                StringMapEntryModify("highway", "cycleway", "cycleway"),
+                StringMapEntryDelete("segregated", "yes"),
+            )
+        )
+    }
+}
+
+private fun verifyAnswer(tags: Map<String, String>, answer: SeparateCycleway, expectedChanges: Array<StringMapEntryChange>) {
+    val cb = StringMapChangesBuilder(tags)
+    answer.applyTo(cb)
+    val changes = cb.create().changes
+    Assertions.assertThat(changes).containsExactlyInAnyOrder(*expectedChanges)
+}
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayParserKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayParserKtTest.kt
new file mode 100644
index 00000000000..fdfaebb3b96
--- /dev/null
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway_separate/SeparateCyclewayParserKtTest.kt
@@ -0,0 +1,68 @@
+package de.westnordost.streetcomplete.osm.cycleway_separate
+
+import de.westnordost.streetcomplete.osm.cycleway_separate.SeparateCycleway.*
+import org.junit.Assert.*
+import org.junit.Test
+
+class SeparateCyclewayParserKtTest {
+
+    @Test fun `parse no cycleway at all`() {
+        assertEquals(null, parse())
+        assertEquals(null, parse("highway" to "residential"))
+    }
+
+    @Test fun `parse no bicyclists allowed on path`() {
+        assertEquals(NONE, parse("highway" to "path"))
+        assertEquals(NONE, parse("highway" to "footway"))
+
+        assertEquals(NONE, parse("highway" to "path", "bicycle" to "no"))
+        assertEquals(NONE, parse("highway" to "footway", "bicycle" to "no"))
+        assertEquals(NONE, parse("highway" to "cycleway", "bicycle" to "no"))
+    }
+
+    @Test fun `parse bicyclists allowed on path`() {
+        assertEquals(ALLOWED, parse("highway" to "path", "bicycle" to "yes"))
+        assertEquals(ALLOWED, parse("highway" to "footway", "bicycle" to "yes"))
+        assertEquals(ALLOWED, parse("highway" to "cycleway", "bicycle" to "yes"))
+    }
+
+    @Test fun `parse cyclists on non-segregated path`() {
+        assertEquals(NON_SEGREGATED, parse("highway" to "path", "bicycle" to "designated", "segregated" to "no"))
+        assertEquals(NON_SEGREGATED, parse("highway" to "footway", "bicycle" to "designated", "segregated" to "no"))
+        assertEquals(NON_SEGREGATED, parse("highway" to "cycleway", "segregated" to "no", "foot" to "yes"))
+        assertEquals(NON_SEGREGATED, parse("highway" to "cycleway", "segregated" to "no", "foot" to "designated"))
+        assertEquals(NON_SEGREGATED, parse("highway" to "cycleway", "bicycle" to "designated", "segregated" to "no", "foot" to "yes"))
+    }
+
+    @Test fun `parse cyclists on segregated path`() {
+        assertEquals(SEGREGATED, parse("highway" to "path", "bicycle" to "designated", "segregated" to "yes"))
+        assertEquals(SEGREGATED, parse("highway" to "footway", "bicycle" to "designated", "segregated" to "yes"))
+
+        assertEquals(SEGREGATED, parse("highway" to "cycleway", "bicycle" to "designated", "segregated" to "yes", "foot" to "yes"))
+        assertEquals(SEGREGATED, parse("highway" to "cycleway", "segregated" to "yes", "foot" to "yes"))
+        assertEquals(SEGREGATED, parse("highway" to "cycleway", "segregated" to "yes", "foot" to "designated"))
+    }
+
+    @Test fun `parse cyclists on exclusive path`() {
+        assertEquals(EXCLUSIVE, parse("highway" to "path", "bicycle" to "designated", "foot" to "no"))
+        assertEquals(EXCLUSIVE, parse("highway" to "footway", "bicycle" to "designated", "foot" to "no"))
+        assertEquals(EXCLUSIVE, parse("highway" to "cycleway"))
+        assertEquals(EXCLUSIVE, parse("highway" to "cycleway", "foot" to "no"))
+
+        assertEquals(EXCLUSIVE, parse("highway" to "cycleway", "sidewalk" to "separate"))
+        assertEquals(EXCLUSIVE, parse("highway" to "cycleway", "sidewalk" to "no"))
+    }
+
+    @Test fun `parse cycleway with sidewalk`() {
+        assertEquals(EXCLUSIVE_WITH_SIDEWALK, parse("highway" to "path", "bicycle" to "designated", "sidewalk" to "left"))
+        assertEquals(EXCLUSIVE_WITH_SIDEWALK, parse("highway" to "footway", "bicycle" to "designated", "sidewalk" to "right"))
+
+        assertEquals(EXCLUSIVE_WITH_SIDEWALK, parse("highway" to "cycleway", "bicycle" to "designated", "sidewalk:left" to "yes"))
+        assertEquals(EXCLUSIVE_WITH_SIDEWALK, parse("highway" to "cycleway", "sidewalk:both" to "yes"))
+        assertEquals(EXCLUSIVE_WITH_SIDEWALK, parse("highway" to "cycleway", "sidewalk:right" to "yes"))
+
+        assertEquals(EXCLUSIVE_WITH_SIDEWALK, parse("highway" to "cycleway", "sidewalk" to "yes"))
+    }
+}
+
+private fun parse(vararg pairs: Pair<String, String>) = createSeparateCycleway(mapOf(*pairs))
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkCreatorKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkCreatorKtTest.kt
new file mode 100644
index 00000000000..53be49cada0
--- /dev/null
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkCreatorKtTest.kt
@@ -0,0 +1,267 @@
+package de.westnordost.streetcomplete.osm.sidewalk
+
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
+import org.assertj.core.api.Assertions
+import org.junit.Test
+
+class SidewalkCreatorKtTest {
+    @Test fun `apply nothing applies nothing`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightSidewalk(null, null),
+            arrayOf()
+        )
+    }
+
+    @Test fun `apply simple values`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightSidewalk(Sidewalk.YES, Sidewalk.YES),
+            arrayOf(StringMapEntryAdd("sidewalk", "both"))
+        )
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightSidewalk(Sidewalk.NO, Sidewalk.NO),
+            arrayOf(StringMapEntryAdd("sidewalk", "no"))
+        )
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightSidewalk(Sidewalk.YES, Sidewalk.NO),
+            arrayOf(StringMapEntryAdd("sidewalk", "left"))
+        )
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightSidewalk(Sidewalk.NO, Sidewalk.YES),
+            arrayOf(StringMapEntryAdd("sidewalk", "right"))
+        )
+    }
+
+    @Test fun `apply value when each side differs`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightSidewalk(Sidewalk.YES, Sidewalk.SEPARATE),
+            arrayOf(
+                StringMapEntryAdd("sidewalk:left", "yes"),
+                StringMapEntryAdd("sidewalk:right", "separate")
+            )
+        )
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightSidewalk(Sidewalk.SEPARATE, Sidewalk.NO),
+            arrayOf(
+                StringMapEntryAdd("sidewalk:left", "separate"),
+                StringMapEntryAdd("sidewalk:right", "no")
+            )
+        )
+    }
+
+    @Test fun `clean up previous tagging`() {
+        verifyAnswer(
+            mapOf(
+                "sidewalk" to "different",
+                "sidewalk:left" to "yes",
+                "sidewalk:right" to "separate",
+                "sidewalk:both" to "yes and separate ;-)"
+            ),
+            LeftAndRightSidewalk(Sidewalk.SEPARATE, Sidewalk.SEPARATE),
+            arrayOf(
+                StringMapEntryModify("sidewalk:both", "yes and separate ;-)", "separate"),
+                StringMapEntryDelete("sidewalk:left", "yes"),
+                StringMapEntryDelete("sidewalk:right", "separate"),
+                StringMapEntryDelete("sidewalk", "different"),
+            )
+        )
+    }
+
+    @Test fun `clean up previous tagging when applying value for each side`() {
+        verifyAnswer(
+            mapOf(
+                "sidewalk" to "both",
+                "sidewalk:both" to "yes",
+            ),
+            LeftAndRightSidewalk(Sidewalk.SEPARATE, Sidewalk.YES),
+            arrayOf(
+                StringMapEntryAdd("sidewalk:left", "separate"),
+                StringMapEntryAdd("sidewalk:right", "yes"),
+                StringMapEntryDelete("sidewalk", "both"),
+                StringMapEntryDelete("sidewalk:both", "yes"),
+            )
+        )
+    }
+
+    @Test fun `updates check date`() {
+        verifyAnswer(
+            mapOf("sidewalk" to "both"),
+            LeftAndRightSidewalk(Sidewalk.YES, Sidewalk.YES),
+            arrayOf(
+                StringMapEntryModify("sidewalk", "both", "both"),
+                StringMapEntryAdd("check_date:sidewalk", nowAsCheckDateString())
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "sidewalk:left" to "separate",
+                "sidewalk:right" to "no"
+            ),
+            LeftAndRightSidewalk(Sidewalk.SEPARATE, Sidewalk.NO),
+            arrayOf(
+                StringMapEntryModify("sidewalk:left", "separate", "separate"),
+                StringMapEntryModify("sidewalk:right", "no", "no"),
+                StringMapEntryAdd("check_date:sidewalk", nowAsCheckDateString())
+            )
+        )
+    }
+
+    @Test fun `apply value only for one side`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightSidewalk(Sidewalk.YES, null),
+            arrayOf(
+                StringMapEntryAdd("sidewalk:left", "yes")
+            )
+        )
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightSidewalk(null, Sidewalk.NO),
+            arrayOf(
+                StringMapEntryAdd("sidewalk:right", "no")
+            )
+        )
+    }
+
+    @Test fun `apply for one side does not touch the other side`() {
+        verifyAnswer(
+            mapOf("sidewalk:left" to "separate"),
+            LeftAndRightSidewalk(null, Sidewalk.YES),
+            arrayOf(
+                StringMapEntryAdd("sidewalk:right", "yes")
+            )
+        )
+        verifyAnswer(
+            mapOf("sidewalk:right" to "yes"),
+            LeftAndRightSidewalk(Sidewalk.NO, null),
+            arrayOf(
+                StringMapEntryAdd("sidewalk", "right"),
+                StringMapEntryDelete("sidewalk:right", "yes"),
+            )
+        )
+    }
+
+    @Test fun `apply for one side does not touch the other side even if it is invalid`() {
+        verifyAnswer(
+            mapOf("sidewalk:left" to "some invalid value"),
+            LeftAndRightSidewalk(null, Sidewalk.YES),
+            arrayOf(
+                StringMapEntryAdd("sidewalk:right", "yes")
+            )
+        )
+        verifyAnswer(
+            mapOf("sidewalk:right" to "another invalid value"),
+            LeftAndRightSidewalk(Sidewalk.NO, null),
+            arrayOf(
+                StringMapEntryAdd("sidewalk:left", "no")
+            )
+        )
+    }
+
+    @Test fun `apply for one side does not change values for the other side even if it was defined for both sides before and invalid`() {
+        verifyAnswer(
+            mapOf("sidewalk:both" to "some invalid value"),
+            LeftAndRightSidewalk(null, Sidewalk.YES),
+            arrayOf(
+                StringMapEntryAdd("sidewalk:right", "yes"),
+                StringMapEntryDelete("sidewalk:both", "some invalid value"),
+                StringMapEntryAdd("sidewalk:left", "some invalid value"),
+            )
+        )
+        verifyAnswer(
+            mapOf("sidewalk:both" to "some invalid value"),
+            LeftAndRightSidewalk(Sidewalk.YES, null),
+            arrayOf(
+                StringMapEntryAdd("sidewalk:left", "yes"),
+                StringMapEntryDelete("sidewalk:both", "some invalid value"),
+                StringMapEntryAdd("sidewalk:right", "some invalid value"),
+            )
+        )
+    }
+
+    @Test fun `apply conflates values`() {
+        verifyAnswer(
+            mapOf(
+                "sidewalk:left" to "yes",
+                "sidewalk:right" to "yes",
+            ),
+            LeftAndRightSidewalk(Sidewalk.YES, null),
+            arrayOf(
+                StringMapEntryDelete("sidewalk:left", "yes"),
+                StringMapEntryDelete("sidewalk:right", "yes"),
+                StringMapEntryAdd("sidewalk", "both"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "sidewalk:right" to "no",
+            ),
+            LeftAndRightSidewalk(Sidewalk.YES, null),
+            arrayOf(
+                StringMapEntryDelete("sidewalk:right", "no"),
+                StringMapEntryAdd("sidewalk", "left"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "sidewalk:right" to "yes",
+            ),
+            LeftAndRightSidewalk(Sidewalk.NO, null),
+            arrayOf(
+                StringMapEntryDelete("sidewalk:right", "yes"),
+                StringMapEntryAdd("sidewalk", "right"),
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "sidewalk:right" to "no",
+            ),
+            LeftAndRightSidewalk(Sidewalk.NO, null),
+            arrayOf(
+                StringMapEntryDelete("sidewalk:right", "no"),
+                StringMapEntryAdd("sidewalk", "no"),
+            )
+        )
+    }
+
+    @Test fun `apply does not conflate values non-yes-no-values`() {
+        verifyAnswer(
+            mapOf(
+                "sidewalk:right" to "separate",
+            ),
+            LeftAndRightSidewalk(Sidewalk.SEPARATE, null),
+            arrayOf(
+                StringMapEntryDelete("sidewalk:right", "separate"),
+                StringMapEntryAdd("sidewalk:both", "separate"),
+            )
+        )
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun `applying invalid left throws exception`() {
+        LeftAndRightSidewalk(Sidewalk.INVALID, null).applyTo(StringMapChangesBuilder(mapOf()))
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun `applying invalid right throws exception`() {
+        LeftAndRightSidewalk(null, Sidewalk.INVALID).applyTo(StringMapChangesBuilder(mapOf()))
+    }
+}
+
+private fun verifyAnswer(tags: Map<String, String>, answer: LeftAndRightSidewalk, expectedChanges: Array<StringMapEntryChange>) {
+    val cb = StringMapChangesBuilder(tags)
+    answer.applyTo(cb)
+    val changes = cb.create().changes
+    Assertions.assertThat(changes).containsExactlyInAnyOrder(*expectedChanges)
+}
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkKtTest.kt
index dd2eb346626..5624f4e83c5 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkKtTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkKtTest.kt
@@ -1,151 +1,9 @@
 package de.westnordost.streetcomplete.osm.sidewalk
 
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.nowAsCheckDateString
-import org.assertj.core.api.Assertions
 import org.junit.Assert.assertEquals
 import org.junit.Test
 
 class SidewalkKtTest {
-    @Test fun `apply simple values`() {
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightSidewalk(Sidewalk.YES, Sidewalk.YES),
-            arrayOf(StringMapEntryAdd("sidewalk", "both"))
-        )
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightSidewalk(Sidewalk.NO, Sidewalk.NO),
-            arrayOf(StringMapEntryAdd("sidewalk", "no"))
-        )
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightSidewalk(Sidewalk.YES, Sidewalk.NO),
-            arrayOf(StringMapEntryAdd("sidewalk", "left"))
-        )
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightSidewalk(Sidewalk.NO, Sidewalk.YES),
-            arrayOf(StringMapEntryAdd("sidewalk", "right"))
-        )
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightSidewalk(Sidewalk.SEPARATE, Sidewalk.SEPARATE),
-            arrayOf(StringMapEntryAdd("sidewalk", "separate"))
-        )
-    }
-
-    @Test fun `apply value when each side differs`() {
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightSidewalk(Sidewalk.YES, Sidewalk.SEPARATE),
-            arrayOf(
-                StringMapEntryAdd("sidewalk:left", "yes"),
-                StringMapEntryAdd("sidewalk:right", "separate")
-            )
-        )
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightSidewalk(Sidewalk.SEPARATE, Sidewalk.NO),
-            arrayOf(
-                StringMapEntryAdd("sidewalk:left", "separate"),
-                StringMapEntryAdd("sidewalk:right", "no")
-            )
-        )
-    }
-
-    @Test fun `clean up previous tagging when applying simple values`() {
-        verifyAnswer(
-            mapOf(
-                "sidewalk:left" to "yes",
-                "sidewalk:right" to "separate",
-                "sidewalk:both" to "yes and separate ;-)",
-                "sidewalk:left:surface" to "jello",
-                "sidewalk:both:oneway" to "yes"
-            ),
-            LeftAndRightSidewalk(Sidewalk.SEPARATE, Sidewalk.SEPARATE),
-            arrayOf(
-                StringMapEntryAdd("sidewalk", "separate"),
-                StringMapEntryDelete("sidewalk:left", "yes"),
-                StringMapEntryDelete("sidewalk:right", "separate"),
-                StringMapEntryDelete("sidewalk:both", "yes and separate ;-)"),
-                StringMapEntryDelete("sidewalk:left:surface", "jello"),
-                StringMapEntryDelete("sidewalk:both:oneway", "yes"),
-            )
-        )
-    }
-
-    @Test fun `clean up previous tagging when applying value for each side`() {
-        verifyAnswer(
-            mapOf(
-                "sidewalk" to "both",
-                "sidewalk:both" to "yes",
-                "sidewalk:left:surface" to "jello",
-                "sidewalk:both:oneway" to "yes"
-            ),
-            LeftAndRightSidewalk(Sidewalk.SEPARATE, Sidewalk.YES),
-            arrayOf(
-                StringMapEntryAdd("sidewalk:left", "separate"),
-                StringMapEntryAdd("sidewalk:right", "yes"),
-                StringMapEntryDelete("sidewalk", "both"),
-                StringMapEntryDelete("sidewalk:both", "yes"),
-                StringMapEntryDelete("sidewalk:left:surface", "jello"),
-                StringMapEntryDelete("sidewalk:both:oneway", "yes"),
-            )
-        )
-    }
-
-    @Test fun `updates check date`() {
-        verifyAnswer(
-            mapOf("sidewalk" to "both"),
-            LeftAndRightSidewalk(Sidewalk.YES, Sidewalk.YES),
-            arrayOf(
-                StringMapEntryModify("sidewalk", "both", "both"),
-                StringMapEntryAdd("check_date:sidewalk", nowAsCheckDateString())
-            )
-        )
-        verifyAnswer(
-            mapOf("sidewalk:left" to "separate", "sidewalk:right" to "no"),
-            LeftAndRightSidewalk(Sidewalk.SEPARATE, Sidewalk.NO),
-            arrayOf(
-                StringMapEntryModify("sidewalk:left", "separate", "separate"),
-                StringMapEntryModify("sidewalk:right", "no", "no"),
-                StringMapEntryAdd("check_date:sidewalk", nowAsCheckDateString())
-            )
-        )
-    }
-
-    @Test fun `apply value only for one side`() {
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightSidewalk(Sidewalk.YES, null),
-            arrayOf(
-                StringMapEntryAdd("sidewalk:left", "yes")
-            )
-        )
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightSidewalk(null, Sidewalk.NO),
-            arrayOf(
-                StringMapEntryAdd("sidewalk:right", "no")
-            )
-        )
-    }
-
-    @Test(expected = IllegalArgumentException::class)
-    fun `applying invalid left throws exception`() {
-        LeftAndRightSidewalk(Sidewalk.INVALID, null).applyTo(StringMapChangesBuilder(mapOf()))
-    }
-
-    @Test(expected = IllegalArgumentException::class)
-    fun `applying invalid right throws exception`() {
-        LeftAndRightSidewalk(null, Sidewalk.INVALID).applyTo(StringMapChangesBuilder(mapOf()))
-    }
-
     @Test fun validOrNullValues() {
         assertEquals(
             LeftAndRightSidewalk(null, null),
@@ -161,10 +19,3 @@ class SidewalkKtTest {
         )
     }
 }
-
-private fun verifyAnswer(tags: Map<String, String>, answer: LeftAndRightSidewalk, expectedChanges: Array<StringMapEntryChange>) {
-    val cb = StringMapChangesBuilder(tags)
-    answer.applyTo(cb)
-    val changes = cb.create().changes
-    Assertions.assertThat(changes).containsExactlyInAnyOrder(*expectedChanges)
-}
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingCreatorKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingCreatorKtTest.kt
new file mode 100644
index 00000000000..2b282b4b730
--- /dev/null
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingCreatorKtTest.kt
@@ -0,0 +1,291 @@
+package de.westnordost.streetcomplete.osm.street_parking
+
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
+import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
+import org.assertj.core.api.Assertions
+import org.junit.Test
+
+class StreetParkingCreatorKtTest {
+
+    @Test fun `apply nothing applies nothing`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(null, null),
+            arrayOf()
+        )
+    }
+
+    @Test fun `apply no parking`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(NoStreetParking, NoStreetParking),
+            arrayOf(StringMapEntryAdd("parking:lane:both", "no"))
+        )
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(NoStreetParking, null),
+            arrayOf(StringMapEntryAdd("parking:lane:left", "no"))
+        )
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(null, NoStreetParking),
+            arrayOf(StringMapEntryAdd("parking:lane:right", "no"))
+        )
+    }
+
+    @Test fun `apply separate parking answer`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(StreetParkingSeparate, StreetParkingSeparate),
+            arrayOf(StringMapEntryAdd("parking:lane:both", "separate"))
+        )
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(StreetParkingSeparate, null),
+            arrayOf(StringMapEntryAdd("parking:lane:left", "separate"))
+        )
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(null, StreetParkingSeparate),
+            arrayOf(StringMapEntryAdd("parking:lane:right", "separate"))
+        )
+    }
+
+    @Test fun `apply parallel parking answer on both sides`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(
+                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.HALF_ON_KERB),
+                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.HALF_ON_KERB)
+            ),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:both", "parallel"),
+                StringMapEntryAdd("parking:lane:both:parallel", "half_on_kerb"),
+            )
+        )
+    }
+
+    @Test fun `apply parallel parking answer with different positions on sides`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(
+                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.ON_STREET),
+                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.HALF_ON_KERB)
+            ),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:both", "parallel"),
+                StringMapEntryAdd("parking:lane:left:parallel", "on_street"),
+                StringMapEntryAdd("parking:lane:right:parallel", "half_on_kerb"),
+            )
+        )
+    }
+
+    @Test fun `apply street side parking answer with different orientations on sides`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(
+                StreetParkingPositionAndOrientation(ParkingOrientation.PERPENDICULAR, ParkingPosition.STREET_SIDE),
+                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.STREET_SIDE)
+            ),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:left", "perpendicular"),
+                StringMapEntryAdd("parking:lane:right", "parallel"),
+                StringMapEntryAdd("parking:lane:left:perpendicular", "street_side"),
+                StringMapEntryAdd("parking:lane:right:parallel", "street_side"),
+            )
+        )
+    }
+
+    @Test fun `apply different parking positions and orientations on sides`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(
+                StreetParkingPositionAndOrientation(ParkingOrientation.DIAGONAL, ParkingPosition.STREET_SIDE),
+                StreetParkingPositionAndOrientation(ParkingOrientation.PERPENDICULAR, ParkingPosition.PAINTED_AREA_ONLY)
+            ),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:left", "diagonal"),
+                StringMapEntryAdd("parking:lane:left:diagonal", "street_side"),
+                StringMapEntryAdd("parking:lane:right", "perpendicular"),
+                StringMapEntryAdd("parking:lane:right:perpendicular", "painted_area_only"),
+            )
+        )
+    }
+
+    @Test fun `applying one side combines with previous tagging of the other side to both-tag`() {
+        verifyAnswer(
+            mapOf("parking:lane:left" to "separate"),
+            LeftAndRightStreetParking(null, StreetParkingSeparate),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:both", "separate"),
+                StringMapEntryDelete("parking:lane:left", "separate"),
+            )
+        )
+        verifyAnswer(
+            mapOf("parking:lane:right" to "separate"),
+            LeftAndRightStreetParking(StreetParkingSeparate, null),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:both", "separate"),
+                StringMapEntryDelete("parking:lane:right", "separate"),
+            )
+        )
+    }
+
+    @Test fun `apply for one side does not touch the other side`() {
+        verifyAnswer(
+            mapOf("parking:lane:left" to "separate"),
+            LeftAndRightStreetParking(null, NoStreetParking),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:right", "no")
+            )
+        )
+        verifyAnswer(
+            mapOf("parking:lane:right" to "no"),
+            LeftAndRightStreetParking(StreetParkingSeparate, null),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:left", "separate")
+            )
+        )
+    }
+
+    @Test fun `apply for one side does not touch the other side even if it is invalid`() {
+        verifyAnswer(
+            mapOf("parking:lane:left" to "narrow"),
+            LeftAndRightStreetParking(null, NoStreetParking),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:right", "no")
+            )
+        )
+        verifyAnswer(
+            mapOf("parking:lane:right" to "narrow"),
+            LeftAndRightStreetParking(StreetParkingSeparate, null),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:left", "separate")
+            )
+        )
+    }
+
+    @Test fun `apply for one side does not change values for the other side even if it was defined for both sides before and invalid`() {
+        verifyAnswer(
+            mapOf(
+                "parking:lane" to "hexagonal",
+                "parking:lane:diagonal" to "on_kerb",
+            ),
+            LeftAndRightStreetParking(null, NoStreetParking),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:right", "no"),
+                StringMapEntryDelete("parking:lane", "hexagonal"),
+                StringMapEntryDelete("parking:lane:diagonal", "on_kerb"),
+                StringMapEntryAdd("parking:lane:left", "hexagonal"),
+                StringMapEntryAdd("parking:lane:left:diagonal", "on_kerb"),
+            )
+        )
+    }
+
+    @Test fun `updates check date`() {
+        verifyAnswer(
+            mapOf("parking:lane:both" to "no"),
+            LeftAndRightStreetParking(NoStreetParking, NoStreetParking),
+            arrayOf(
+                StringMapEntryModify("parking:lane:both", "no", "no"),
+                StringMapEntryAdd("check_date:parking:lane", nowAsCheckDateString())
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "parking:lane:both" to "parallel",
+                "parking:lane:left:parallel" to "half_on_kerb",
+                "parking:lane:right:parallel" to "on_kerb",
+                "parking:condition:left" to "free",
+            ),
+            LeftAndRightStreetParking(
+                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.HALF_ON_KERB),
+                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.ON_KERB)
+            ),
+            arrayOf(
+                StringMapEntryModify("parking:lane:both", "parallel", "parallel"),
+                StringMapEntryModify("parking:lane:left:parallel", "half_on_kerb", "half_on_kerb"),
+                StringMapEntryModify("parking:lane:right:parallel", "on_kerb", "on_kerb"),
+                StringMapEntryAdd("check_date:parking:lane", nowAsCheckDateString())
+            )
+        )
+    }
+
+    @Test fun `clean up previous tagging when applying value for each side`() {
+        verifyAnswer(
+            mapOf(
+                "parking:lane:both" to "parallel",
+                "parking:lane:left:parallel" to "half_on_kerb",
+                "parking:lane:right:parallel" to "on_kerb",
+            ),
+            LeftAndRightStreetParking(
+                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.ON_STREET),
+                StreetParkingPositionAndOrientation(ParkingOrientation.DIAGONAL, ParkingPosition.ON_STREET)
+            ),
+            arrayOf(
+                StringMapEntryDelete("parking:lane:both", "parallel"),
+                StringMapEntryAdd("parking:lane:left", "parallel"),
+                StringMapEntryAdd("parking:lane:right", "diagonal"),
+                StringMapEntryModify("parking:lane:left:parallel", "half_on_kerb", "on_street"),
+                StringMapEntryDelete("parking:lane:right:parallel", "on_kerb"),
+                StringMapEntryAdd("parking:lane:right:diagonal", "on_street"),
+            )
+        )
+    }
+
+    @Test fun `tag only on one side`() {
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(
+                null,
+                StreetParkingPositionAndOrientation(ParkingOrientation.DIAGONAL, ParkingPosition.ON_STREET)
+            ),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:right", "diagonal"),
+                StringMapEntryAdd("parking:lane:right:diagonal", "on_street")
+            )
+        )
+        verifyAnswer(
+            mapOf(),
+            LeftAndRightStreetParking(
+                StreetParkingPositionAndOrientation(ParkingOrientation.DIAGONAL, ParkingPosition.ON_STREET),
+                null
+            ),
+            arrayOf(
+                StringMapEntryAdd("parking:lane:left", "diagonal"),
+                StringMapEntryAdd("parking:lane:left:diagonal", "on_street")
+            )
+        )
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun `applying incomplete left throws exception`() {
+        LeftAndRightStreetParking(IncompleteStreetParking, null).applyTo(StringMapChangesBuilder(mapOf()))
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun `applying incomplete right throws exception`() {
+        LeftAndRightStreetParking(null, IncompleteStreetParking).applyTo(StringMapChangesBuilder(mapOf()))
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun `applying unknown left throws exception`() {
+        LeftAndRightStreetParking(UnknownStreetParking, null).applyTo(StringMapChangesBuilder(mapOf()))
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun `applying unknown right throws exception`() {
+        LeftAndRightStreetParking(null, UnknownStreetParking).applyTo(StringMapChangesBuilder(mapOf()))
+    }
+}
+
+private fun verifyAnswer(tags: Map<String, String>, answer: LeftAndRightStreetParking, expectedChanges: Array<StringMapEntryChange>) {
+    val cb = StringMapChangesBuilder(tags)
+    answer.applyTo(cb)
+    val changes = cb.create().changes
+    Assertions.assertThat(changes).containsExactlyInAnyOrder(*expectedChanges)
+}
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingKtTest.kt
index 0dd2cb67b6d..4ecd92dbcf9 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingKtTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingKtTest.kt
@@ -1,241 +1,9 @@
 package de.westnordost.streetcomplete.osm.street_parking
 
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.nowAsCheckDateString
-import org.assertj.core.api.Assertions
 import org.junit.Assert.assertEquals
 import org.junit.Test
 
 class StreetParkingTest {
-
-    @Test fun `apply no parking`() {
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(NoStreetParking, NoStreetParking),
-            arrayOf(StringMapEntryAdd("parking:lane:both", "no"))
-        )
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(NoStreetParking, null),
-            arrayOf(StringMapEntryAdd("parking:lane:left", "no"))
-        )
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(null, NoStreetParking),
-            arrayOf(StringMapEntryAdd("parking:lane:right", "no"))
-        )
-    }
-
-    @Test fun `apply separate parking answer`() {
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(StreetParkingSeparate, StreetParkingSeparate),
-            arrayOf(StringMapEntryAdd("parking:lane:both", "separate"))
-        )
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(StreetParkingSeparate, null),
-            arrayOf(StringMapEntryAdd("parking:lane:left", "separate"))
-        )
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(null, StreetParkingSeparate),
-            arrayOf(StringMapEntryAdd("parking:lane:right", "separate"))
-        )
-    }
-
-    @Test fun `apply parallel parking answer on both sides`() {
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(
-                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.HALF_ON_KERB),
-                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.HALF_ON_KERB)
-            ),
-            arrayOf(
-                StringMapEntryAdd("parking:lane:both", "parallel"),
-                StringMapEntryAdd("parking:lane:both:parallel", "half_on_kerb"),
-            )
-        )
-    }
-
-    @Test fun `apply parallel parking answer with different positions on sides`() {
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(
-                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.ON_STREET),
-                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.HALF_ON_KERB)
-            ),
-            arrayOf(
-                StringMapEntryAdd("parking:lane:both", "parallel"),
-                StringMapEntryAdd("parking:lane:left:parallel", "on_street"),
-                StringMapEntryAdd("parking:lane:right:parallel", "half_on_kerb"),
-            )
-        )
-    }
-
-    @Test fun `apply street side parking answer with different orientations on sides`() {
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(
-                StreetParkingPositionAndOrientation(ParkingOrientation.PERPENDICULAR, ParkingPosition.STREET_SIDE),
-                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.STREET_SIDE)
-            ),
-            arrayOf(
-                StringMapEntryAdd("parking:lane:left", "perpendicular"),
-                StringMapEntryAdd("parking:lane:right", "parallel"),
-                StringMapEntryAdd("parking:lane:left:perpendicular", "street_side"),
-                StringMapEntryAdd("parking:lane:right:parallel", "street_side"),
-            )
-        )
-    }
-
-    @Test fun `apply different parking positions and orientations on sides`() {
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(
-                StreetParkingPositionAndOrientation(ParkingOrientation.DIAGONAL, ParkingPosition.STREET_SIDE),
-                StreetParkingPositionAndOrientation(ParkingOrientation.PERPENDICULAR, ParkingPosition.PAINTED_AREA_ONLY)
-            ),
-            arrayOf(
-                StringMapEntryAdd("parking:lane:left", "diagonal"),
-                StringMapEntryAdd("parking:lane:left:diagonal", "street_side"),
-                StringMapEntryAdd("parking:lane:right", "perpendicular"),
-                StringMapEntryAdd("parking:lane:right:perpendicular", "painted_area_only"),
-            )
-        )
-    }
-
-    @Test fun `applying one side combines with previous tagging of the other side to both-tag`() {
-        verifyAnswer(
-            mapOf("parking:lane:left" to "separate"),
-            LeftAndRightStreetParking(null, StreetParkingSeparate),
-            arrayOf(
-                StringMapEntryAdd("parking:lane:both", "separate"),
-                StringMapEntryDelete("parking:lane:left", "separate"),
-            )
-        )
-        verifyAnswer(
-            mapOf("parking:lane:right" to "separate"),
-            LeftAndRightStreetParking(StreetParkingSeparate, null),
-            arrayOf(
-                StringMapEntryAdd("parking:lane:both", "separate"),
-                StringMapEntryDelete("parking:lane:right", "separate"),
-            )
-        )
-    }
-
-    @Test fun `changing one side replaces both-tag`() {
-        verifyAnswer(
-            mapOf("parking:lane:both" to "separate"),
-            LeftAndRightStreetParking(null, NoStreetParking),
-            arrayOf(
-                StringMapEntryAdd("parking:lane:left", "separate"),
-                StringMapEntryAdd("parking:lane:right", "no"),
-                StringMapEntryDelete("parking:lane:both", "separate"),
-            )
-        )
-    }
-
-    @Test fun `updates check date`() {
-        verifyAnswer(
-            mapOf("parking:lane:both" to "no"),
-            LeftAndRightStreetParking(NoStreetParking, NoStreetParking),
-            arrayOf(
-                StringMapEntryModify("parking:lane:both", "no", "no"),
-                StringMapEntryAdd("check_date:parking:lane", nowAsCheckDateString())
-            )
-        )
-        verifyAnswer(
-            mapOf(
-                "parking:lane:both" to "parallel",
-                "parking:lane:left:parallel" to "half_on_kerb",
-                "parking:lane:right:parallel" to "on_kerb",
-                "parking:condition:left" to "free",
-            ),
-            LeftAndRightStreetParking(
-                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.HALF_ON_KERB),
-                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.ON_KERB)
-            ),
-            arrayOf(
-                StringMapEntryModify("parking:lane:both", "parallel", "parallel"),
-                StringMapEntryModify("parking:lane:left:parallel", "half_on_kerb", "half_on_kerb"),
-                StringMapEntryModify("parking:lane:right:parallel", "on_kerb", "on_kerb"),
-                StringMapEntryAdd("check_date:parking:lane", nowAsCheckDateString())
-            )
-        )
-    }
-
-    @Test fun `clean up previous tagging when applying value for each side`() {
-        verifyAnswer(
-            mapOf(
-                "parking:lane:both" to "parallel",
-                "parking:lane:left:parallel" to "half_on_kerb",
-                "parking:lane:right:parallel" to "on_kerb",
-            ),
-            LeftAndRightStreetParking(
-                StreetParkingPositionAndOrientation(ParkingOrientation.PARALLEL, ParkingPosition.ON_STREET),
-                StreetParkingPositionAndOrientation(ParkingOrientation.DIAGONAL, ParkingPosition.ON_STREET)
-            ),
-            arrayOf(
-                StringMapEntryDelete("parking:lane:both", "parallel"),
-                StringMapEntryAdd("parking:lane:left", "parallel"),
-                StringMapEntryAdd("parking:lane:right", "diagonal"),
-                StringMapEntryModify("parking:lane:left:parallel", "half_on_kerb", "on_street"),
-                StringMapEntryDelete("parking:lane:right:parallel", "on_kerb"),
-                StringMapEntryAdd("parking:lane:right:diagonal", "on_street"),
-            )
-        )
-    }
-
-    @Test fun `tag only on one side`() {
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(
-                null,
-                StreetParkingPositionAndOrientation(ParkingOrientation.DIAGONAL, ParkingPosition.ON_STREET)
-            ),
-            arrayOf(
-                StringMapEntryAdd("parking:lane:right", "diagonal"),
-                StringMapEntryAdd("parking:lane:right:diagonal", "on_street")
-            )
-        )
-        verifyAnswer(
-            mapOf(),
-            LeftAndRightStreetParking(
-                StreetParkingPositionAndOrientation(ParkingOrientation.DIAGONAL, ParkingPosition.ON_STREET),
-                null
-            ),
-            arrayOf(
-                StringMapEntryAdd("parking:lane:left", "diagonal"),
-                StringMapEntryAdd("parking:lane:left:diagonal", "on_street")
-            )
-        )
-    }
-
-    @Test(expected = IllegalArgumentException::class)
-    fun `applying incomplete left throws exception`() {
-        LeftAndRightStreetParking(IncompleteStreetParking, null).applyTo(StringMapChangesBuilder(mapOf()))
-    }
-
-    @Test(expected = IllegalArgumentException::class)
-    fun `applying incomplete right throws exception`() {
-        LeftAndRightStreetParking(null, IncompleteStreetParking).applyTo(StringMapChangesBuilder(mapOf()))
-    }
-
-    @Test(expected = IllegalArgumentException::class)
-    fun `applying unknown left throws exception`() {
-        LeftAndRightStreetParking(UnknownStreetParking, null).applyTo(StringMapChangesBuilder(mapOf()))
-    }
-
-    @Test(expected = IllegalArgumentException::class)
-    fun `applying unknown right throws exception`() {
-        LeftAndRightStreetParking(null, UnknownStreetParking).applyTo(StringMapChangesBuilder(mapOf()))
-    }
-
     @Test fun validOrNullValues() {
         for (invalidParking in listOf(IncompleteStreetParking, UnknownStreetParking)) {
             assertEquals(
@@ -253,10 +21,3 @@ class StreetParkingTest {
         }
     }
 }
-
-private fun verifyAnswer(tags: Map<String, String>, answer: LeftAndRightStreetParking, expectedChanges: Array<StringMapEntryChange>) {
-    val cb = StringMapChangesBuilder(tags)
-    answer.applyTo(cb)
-    val changes = cb.create().changes
-    Assertions.assertThat(changes).containsExactlyInAnyOrder(*expectedChanges)
-}
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/OsmElementQuestType.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/OsmElementQuestType.kt
index 380c7ef6067..6342f49edde 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/OsmElementQuestType.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/OsmElementQuestType.kt
@@ -2,12 +2,14 @@ package de.westnordost.streetcomplete.quests
 
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
+import de.westnordost.streetcomplete.data.osm.geometry.ElementPointGeometry
+import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
 import org.assertj.core.api.Assertions.assertThat
 
 fun <T> OsmElementQuestType<T>.verifyAnswer(tags: Map<String, String>, answer: T, vararg expectedChanges: StringMapEntryChange) {
     val cb = StringMapChangesBuilder(tags)
-    this.applyAnswerTo(answer, cb, 0)
+    this.applyAnswerTo(answer, cb, ElementPointGeometry(LatLon(0.0, 0.0)), 0)
     val changes = cb.create().changes
     assertThat(changes).containsExactlyInAnyOrder(*expectedChanges)
 }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt
index a1dfb7ba841..40b8f7f54f1 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt
@@ -4,26 +4,8 @@ import de.westnordost.countryboundaries.CountryBoundaries
 import de.westnordost.streetcomplete.data.meta.CountryInfo
 import de.westnordost.streetcomplete.data.meta.CountryInfos
 import de.westnordost.streetcomplete.data.meta.getByLocation
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
-import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
 import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.ADVISORY_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.BUSWAY
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.DUAL_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.DUAL_TRACK
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.EXCLUSIVE_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.NONE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.PICTOGRAMS
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SEPARATE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SIDEWALK_EXPLICIT
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SUGGESTION_LANE
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.TRACK
-import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNSPECIFIED_LANE
-import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.TestMapDataWithGeometry
-import de.westnordost.streetcomplete.quests.verifyAnswer
 import de.westnordost.streetcomplete.testutils.mock
 import de.westnordost.streetcomplete.testutils.on
 import de.westnordost.streetcomplete.testutils.p
@@ -223,8 +205,8 @@ class AddCyclewayTest {
     @Test fun `not applicable to road with cycleway that is tagged with an unknown + invalid value`() {
         val way = way(1L, listOf(1, 2, 3), mapOf(
             "highway" to "primary",
-            "cycleway:left" to "yes",
-            "cycleway:right" to "doorzone",
+            "cycleway:left" to "yes", // invalid
+            "cycleway:right" to "doorzone2", // unknown
         ))
         val mapData = TestMapDataWithGeometry(listOf(way))
 
@@ -284,344 +266,4 @@ class AddCyclewayTest {
         // because we don't know if we are in Belgium
         assertNull(questType.isApplicableTo(way))
     }
-
-    @Test fun `apply cycleway lane answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(EXCLUSIVE_LANE),
-            StringMapEntryAdd("cycleway:both", "lane"),
-            StringMapEntryAdd("cycleway:both:lane", "exclusive")
-        )
-    }
-
-    @Test fun `apply advisory lane answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(ADVISORY_LANE),
-            StringMapEntryAdd("cycleway:both", "lane"),
-            StringMapEntryAdd("cycleway:both:lane", "advisory")
-        )
-    }
-
-    @Test fun `apply cycleway track answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(TRACK),
-            StringMapEntryAdd("cycleway:both", "track")
-        )
-    }
-
-    @Test fun `apply unspecified cycle lane answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(UNSPECIFIED_LANE),
-            StringMapEntryAdd("cycleway:both", "lane")
-        )
-    }
-
-    @Test fun `apply bus lane answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(BUSWAY),
-            StringMapEntryAdd("cycleway:both", "share_busway")
-        )
-    }
-
-    @Test fun `apply pictogram lane answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(PICTOGRAMS),
-            StringMapEntryAdd("cycleway:both", "shared_lane"),
-            StringMapEntryAdd("cycleway:both:lane", "pictogram")
-        )
-    }
-
-    @Test fun `apply suggestion lane answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(PICTOGRAMS),
-            StringMapEntryAdd("cycleway:both", "shared_lane"),
-            StringMapEntryAdd("cycleway:both:lane", "pictogram")
-        )
-    }
-
-    @Test fun `apply no cycleway answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(NONE),
-            StringMapEntryAdd("cycleway:both", "no")
-        )
-    }
-
-    @Test fun `apply cycleway on sidewalk answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(SIDEWALK_EXPLICIT),
-            StringMapEntryAdd("cycleway:both", "track"),
-            StringMapEntryAdd("sidewalk", "both"),
-            StringMapEntryAdd("cycleway:both:segregated", "no")
-        )
-    }
-
-    @Test fun `apply cycleway on sidewalk answer only on one side`() {
-        questType.verifyAnswer(
-            CyclewayAnswer(CyclewaySide(SIDEWALK_EXPLICIT), CyclewaySide(NONE)),
-            StringMapEntryAdd("cycleway:left", "track"),
-            StringMapEntryAdd("cycleway:right", "no"),
-            // not this: StringMapEntryAdd("sidewalk", "both"),
-            StringMapEntryAdd("cycleway:left:segregated", "no")
-        )
-    }
-
-    private fun bothSidesAnswer(bothSides: Cycleway): CyclewayAnswer {
-        val side = CyclewaySide(bothSides)
-        return CyclewayAnswer(side, side)
-    }
-
-    @Test fun `apply separate cycleway answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(SEPARATE),
-            StringMapEntryAdd("cycleway:both", "separate")
-        )
-    }
-
-    @Test fun `apply dual cycle track answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(DUAL_TRACK),
-            StringMapEntryAdd("cycleway:both", "track"),
-            StringMapEntryAdd("cycleway:both:oneway", "no")
-        )
-    }
-
-    @Test fun `apply dual cycle lane answer`() {
-        questType.verifyAnswer(
-            bothSidesAnswer(DUAL_LANE),
-            StringMapEntryAdd("cycleway:both", "lane"),
-            StringMapEntryAdd("cycleway:both:oneway", "no"),
-            StringMapEntryAdd("cycleway:both:lane", "exclusive")
-        )
-    }
-
-    @Test fun `apply answer where left and right side are different`() {
-        questType.verifyAnswer(
-            CyclewayAnswer(CyclewaySide(TRACK), CyclewaySide(NONE)),
-            StringMapEntryAdd("cycleway:left", "track"),
-            StringMapEntryAdd("cycleway:right", "no")
-        )
-    }
-
-    @Test fun `apply answer where there exists a cycleway in opposite direction of oneway`() {
-        // this would be a street that has tracks on both sides but is oneway=yes (in countries with
-        // right hand traffic)
-        questType.verifyAnswer(
-            CyclewayAnswer(CyclewaySide(TRACK, -1), CyclewaySide(TRACK), true),
-            StringMapEntryAdd("cycleway:left", "track"),
-            StringMapEntryAdd("cycleway:left:oneway", "-1"),
-            StringMapEntryAdd("cycleway:right", "track"),
-            StringMapEntryAdd("oneway:bicycle", "no")
-        )
-    }
-
-    @Test fun `apply answer where there exists a cycleway in opposite direction of backward oneway`() {
-        // this would be a street that has lanes on both sides but is oneway=-1 (in countries with
-        // right hand traffic)
-        questType.verifyAnswer(
-            CyclewayAnswer(CyclewaySide(TRACK, +1), CyclewaySide(TRACK), true),
-            StringMapEntryAdd("cycleway:left", "track"),
-            StringMapEntryAdd("cycleway:left:oneway", "yes"),
-            StringMapEntryAdd("cycleway:right", "track"),
-            StringMapEntryAdd("oneway:bicycle", "no")
-        )
-    }
-
-    @Test fun `apply answer for both deletes any previous answers given for left, right, general`() {
-        questType.verifyAnswer(
-            mapOf(
-                "cycleway:left" to "lane",
-                "cycleway:left:lane" to "advisory",
-                "cycleway:left:segregated" to "maybe",
-                "cycleway:left:oneway" to "yes",
-                "sidewalk:left:bicycle" to "yes",
-                "cycleway:right" to "shared_lane",
-                "cycleway:right:lane" to "pictogram",
-                "cycleway:right:segregated" to "definitely",
-                "cycleway:right:oneway" to "yes",
-                "sidewalk:right:bicycle" to "yes",
-                "cycleway" to "shared_lane",
-                "cycleway:lane" to "pictogram",
-                "cycleway:segregated" to "definitely",
-                "cycleway:oneway" to "yes",
-                "sidewalk:bicycle" to "yes"
-            ),
-            bothSidesAnswer(TRACK),
-            StringMapEntryAdd("cycleway:both", "track"),
-            StringMapEntryDelete("cycleway:left", "lane"),
-            StringMapEntryDelete("cycleway:left:lane", "advisory"),
-            StringMapEntryDelete("cycleway:left:segregated", "maybe"),
-            StringMapEntryDelete("cycleway:left:oneway", "yes"),
-            StringMapEntryDelete("sidewalk:left:bicycle", "yes"),
-            StringMapEntryDelete("cycleway:right", "shared_lane"),
-            StringMapEntryDelete("cycleway:right:lane", "pictogram"),
-            StringMapEntryDelete("cycleway:right:segregated", "definitely"),
-            StringMapEntryDelete("cycleway:right:oneway", "yes"),
-            StringMapEntryDelete("sidewalk:right:bicycle", "yes"),
-            StringMapEntryDelete("cycleway", "shared_lane"),
-            StringMapEntryDelete("cycleway:lane", "pictogram"),
-            StringMapEntryDelete("cycleway:segregated", "definitely"),
-            StringMapEntryDelete("cycleway:oneway", "yes"),
-            StringMapEntryDelete("sidewalk:bicycle", "yes")
-        )
-    }
-
-    @Test fun `apply answer for left, right deletes any previous answers given for both, general`() {
-        questType.verifyAnswer(
-            mapOf(
-                "cycleway:both" to "shared_lane",
-                "cycleway:both:lane" to "pictogram",
-                "cycleway:both:segregated" to "definitely",
-                "cycleway:both:oneway" to "yes",
-                "sidewalk:both:bicycle" to "yes",
-                "cycleway" to "shared_lane",
-                "cycleway:lane" to "pictogram",
-                "cycleway:segregated" to "definitely",
-                "cycleway:oneway" to "yes",
-                "sidewalk:bicycle" to "yes"
-            ),
-            CyclewayAnswer(CyclewaySide(TRACK), CyclewaySide(NONE), false),
-            StringMapEntryAdd("cycleway:left", "track"),
-            StringMapEntryAdd("cycleway:right", "no"),
-            StringMapEntryDelete("cycleway:both", "shared_lane"),
-            StringMapEntryDelete("cycleway:both:lane", "pictogram"),
-            StringMapEntryDelete("cycleway:both:segregated", "definitely"),
-            StringMapEntryDelete("cycleway:both:oneway", "yes"),
-            StringMapEntryDelete("sidewalk:both:bicycle", "yes"),
-            StringMapEntryDelete("cycleway", "shared_lane"),
-            StringMapEntryDelete("cycleway:lane", "pictogram"),
-            StringMapEntryDelete("cycleway:segregated", "definitely"),
-            StringMapEntryDelete("cycleway:oneway", "yes"),
-            StringMapEntryDelete("sidewalk:bicycle", "yes")
-        )
-    }
-
-    @Test fun `deletes lane subkey when new answer is not a lane`() {
-        questType.verifyAnswer(
-            mapOf(
-                "cycleway:both" to "lane",
-                "cycleway:both:lane" to "exclusive"
-            ),
-            bothSidesAnswer(TRACK),
-            StringMapEntryModify("cycleway:both", "lane", "track"),
-            StringMapEntryDelete("cycleway:both:lane", "exclusive")
-        )
-    }
-
-    @Test fun `deletes shared lane subkey when new answer is not a lane`() {
-        questType.verifyAnswer(
-            mapOf(
-                "cycleway:both" to "shared_lane",
-                "cycleway:both:lane" to "pictogram"
-            ),
-            bothSidesAnswer(TRACK),
-            StringMapEntryModify("cycleway:both", "shared_lane", "track"),
-            StringMapEntryDelete("cycleway:both:lane", "pictogram")
-        )
-    }
-
-    @Test fun `deletes dual lane tag when new answer is not a dual lane`() {
-        questType.verifyAnswer(
-            mapOf(
-                "cycleway:both" to "lane",
-                "cycleway:both:lane" to "exclusive",
-                "cycleway:both:oneway" to "no"
-            ),
-            bothSidesAnswer(EXCLUSIVE_LANE),
-            StringMapEntryModify("cycleway:both", "lane", "lane"),
-            StringMapEntryModify("cycleway:both:lane", "exclusive", "exclusive"),
-            StringMapEntryDelete("cycleway:both:oneway", "no")
-        )
-    }
-
-    @Test fun `modifies lane subkey when new answer is different lane`() {
-        questType.verifyAnswer(
-            mapOf(
-                "cycleway:both" to "shared_lane",
-                "cycleway:both:lane" to "pictogram"
-            ),
-            bothSidesAnswer(SUGGESTION_LANE),
-            StringMapEntryModify("cycleway:both", "shared_lane", "shared_lane"),
-            StringMapEntryModify("cycleway:both:lane", "pictogram", "advisory")
-        )
-    }
-
-    @Test fun `deletes dual track tag when new answer is not a dual track`() {
-        questType.verifyAnswer(
-            mapOf(
-                "cycleway:both" to "track",
-                "cycleway:both:oneway" to "no"
-            ),
-            bothSidesAnswer(TRACK),
-            StringMapEntryModify("cycleway:both", "track", "track"),
-            StringMapEntryDelete("cycleway:both:oneway", "no")
-        )
-    }
-
-    @Test fun `modify segregated tag if new answer is now segregated`() {
-        questType.verifyAnswer(
-            mapOf(
-                "cycleway:both" to "track",
-                "cycleway:both:segregated" to "no"
-            ),
-            bothSidesAnswer(TRACK),
-            StringMapEntryModify("cycleway:both", "track", "track"),
-            StringMapEntryModify("cycleway:both:segregated", "no", "yes")
-        )
-    }
-
-    @Test fun `modify segregated tag if new answer is now not segregated`() {
-        questType.verifyAnswer(
-            mapOf(
-                "sidewalk" to "both",
-                "cycleway:both" to "track",
-                "cycleway:both:segregated" to "yes"
-            ),
-            bothSidesAnswer(SIDEWALK_EXPLICIT),
-            StringMapEntryModify("cycleway:both", "track", "track"),
-            StringMapEntryModify("sidewalk", "both", "both"),
-            StringMapEntryModify("cycleway:both:segregated", "yes", "no")
-        )
-    }
-
-    @Test fun `delete segregated tag if new answer is not a track or on sidewalk`() {
-        questType.verifyAnswer(
-            mapOf(
-                "cycleway:both" to "track",
-                "cycleway:both:segregated" to "no"
-            ),
-            bothSidesAnswer(BUSWAY),
-            StringMapEntryModify("cycleway:both", "track", "share_busway"),
-            StringMapEntryDelete("cycleway:both:segregated", "no")
-        )
-    }
-
-    @Test fun `sets check date if nothing changed`() {
-        questType.verifyAnswer(
-            mapOf("cycleway:both" to "track"),
-            bothSidesAnswer(TRACK),
-            StringMapEntryModify("cycleway:both", "track", "track"),
-            StringMapEntryAdd("check_date:cycleway", nowAsCheckDateString())
-        )
-    }
-
-    @Test fun `updates check date if nothing changed`() {
-        questType.verifyAnswer(
-            mapOf("cycleway:both" to "track", "check_date:cycleway" to "2000-11-11"),
-            bothSidesAnswer(TRACK),
-            StringMapEntryModify("cycleway:both", "track", "track"),
-            StringMapEntryModify("check_date:cycleway", "2000-11-11", nowAsCheckDateString())
-        )
-    }
-
-    @Test fun `remove oneway bicycle no tag if road is also a oneway for bicycles now`() {
-        questType.verifyAnswer(
-            mapOf(
-                "cycleway:both" to "no",
-                "oneway" to "yes",
-                "oneway:bicycle" to "no"
-            ),
-            bothSidesAnswer(NONE),
-            StringMapEntryModify("cycleway:both", "no", "no"),
-            StringMapEntryDelete("oneway:bicycle", "no")
-        )
-    }
 }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkTest.kt
index 3ed12bde718..30a1f1121ca 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkTest.kt
@@ -191,68 +191,4 @@ class AddSidewalkTest {
         assertEquals(1, questType.getApplicableElements(mapData).toList().size)
         assertNull(questType.isApplicableTo(road))
     }
-
-    @Test fun `apply no sidewalk answer`() {
-        questType.verifyAnswer(
-            LeftAndRightSidewalk(left = NO, right = NO),
-            StringMapEntryAdd("sidewalk", "no")
-        )
-    }
-
-    @Test fun `apply sidewalk left answer`() {
-        questType.verifyAnswer(
-            LeftAndRightSidewalk(left = YES, right = NO),
-            StringMapEntryAdd("sidewalk", "left")
-        )
-    }
-
-    @Test fun `apply sidewalk right answer`() {
-        questType.verifyAnswer(
-            LeftAndRightSidewalk(left = NO, right = YES),
-            StringMapEntryAdd("sidewalk", "right")
-        )
-    }
-
-    @Test fun `apply sidewalk on both sides answer`() {
-        questType.verifyAnswer(
-            LeftAndRightSidewalk(left = YES, right = YES),
-            StringMapEntryAdd("sidewalk", "both")
-        )
-    }
-
-    @Test fun `apply separate sidewalk answer`() {
-        questType.verifyAnswer(
-            LeftAndRightSidewalk(left = SEPARATE, right = SEPARATE),
-            StringMapEntryAdd("sidewalk", "separate")
-        )
-    }
-
-    @Test fun `apply separate sidewalk on one side answer`() {
-        questType.verifyAnswer(
-            LeftAndRightSidewalk(left = YES, right = SEPARATE),
-            StringMapEntryAdd("sidewalk:left", "yes"),
-            StringMapEntryAdd("sidewalk:right", "separate"),
-        )
-
-        questType.verifyAnswer(
-            LeftAndRightSidewalk(left = SEPARATE, right = NO),
-            StringMapEntryAdd("sidewalk:left", "separate"),
-            StringMapEntryAdd("sidewalk:right", "no"),
-        )
-    }
-
-    @Test fun `replace incomplete sidewalk tagging`() {
-        questType.verifyAnswer(
-            mapOf("sidewalk:left" to "yes"),
-            LeftAndRightSidewalk(left = YES, right = NO),
-            StringMapEntryAdd("sidewalk", "left"),
-            StringMapEntryDelete("sidewalk:left", "yes")
-        )
-        questType.verifyAnswer(
-            mapOf("sidewalk:left" to "yes"),
-            LeftAndRightSidewalk(left = YES, right = SEPARATE),
-            StringMapEntryModify("sidewalk:left", "yes", "yes"),
-            StringMapEntryAdd("sidewalk:right", "separate"),
-        )
-    }
 }