Avoid or include a large amount of POIs in route-planning

Hello there!

I am new to Graphhopper and wanted to ask how I should best tackle two specific use-cases:

I want to create a small app that allows users to plan routes from point A to B, just within one city (Vienna, Austria). Within this city, I have a large amount of points with lat/long, around 228.000, of different categories.

The route planning should either:

  • Avoid the vicinity of certain categories of points along the route, if possible.
  • Visit certain categories of points along the route, if it is not a big detour.

One route planning would only target a few categories, e.g. avoid points of categories A, B, C, D (11.153 points), or visit points of category F (9.400 points), along a route from A to B.

I thought I would start with avoiding certain points. I have looked at the blog post Examples for customizing routes, where it shows how a single polygon can be avoided.

Just to get a Proof-of-Concept going, I have tried to create a custom model to avoid multiple polygons. More specifically, I have drawn a 10m square around all points from one category and included some of them in the custom model. It looks like this:

{
    "priority": [
      {
        "if": "in_avoid",
        "multiply_by": "0"
      }
    ],
    "areas": {
      "type": "FeatureCollection",
      "features": [
        {
          "type": "Feature",
          "id": "avoid",
          "geometry": {
            "type": "Polygon",
            "coordinates": [
              [
                [
                  16.302208745133793,
                  48.223221470959245
                ],
                [
                  16.302478416713324,
                  48.223221470959245
                ],
                [
                  16.302478416713324,
                  48.22340113401607
                ],
                [
                  16.302208745133793,
                  48.22340113401607
                ],
                [
                  16.302208745133793,
                  48.223221470959245
                ]
              ],
              [
                [
                  16.302200703283482,
                  48.223171196872364
                ],
                [
                  16.30247037459815,
                  48.223171196872364
                ],
                [
                  16.30247037459815,
                  48.22335085992919
                ],
                [
                  16.302200703283482,
                  48.22335085992919
                ],
                [
                  16.302200703283482,
                  48.223171196872364
                ]
              ],
              ....
            ]
          }
        }
      ]
    }
  }

But while the squares denoting the points are shown successfully on the map, it doesn’t seem to work. See this demo.

Additionally, I was only able to include a small number of points in the request, as it results in a status 400 error: “Custom model cannot use more than 100 000 characters.” or 414: “Request-URI Too Large” very quickly. Of course, it doesn’t make sense to submit ten-thousands of polygons with each request. It was just meant as a POC, to see if I could avoid multiple points with Graphhopper. Could someone point out what I am doing wrong? How do I successfully avoid multiple squares? Is there a different, more suitable approach?

For any further development, I would need to setup my own Graphhopper instance, where the points with categories are already saved on the server and the request only submits “avoid category A,B,C” or “include category F”. Is something like this possible? Can you add a dataset of polygons, each with a category property to the Graphhopper instance, and then configure a request to include or avoid those polygons based on their property?

I am thankful for any pointers in the right direction :+1:

But while the squares denoting the points are shown successfully on the map

As stated in the documentation: for an “area” at the moment currently only a single Polygon is supported. However you could create multiple areas and use them in the custom model for exclusion/avoidance.

Additionally, I was only able to include a small number of points in the request, as it results in a status 400 error: “Custom model cannot use more than 100 000 characters.”

We placed a limit for the custom model at some point to avoid certain types of attacks for public facing GraphHopper services. You can reduce the precision of the coordinates or reduce the number of points per polygon to avoid this.

The “Request-URI Too Large” error should not happen as the /route endpoint with custom_model is only POST request. (probably you tried GH Maps? Then this error won’t occur in production)

Hello @karussell, thanks for the answer!

I have just tried creating an area for each point, and it seems to work for a limited number of points. (As a sidenote, is there a way to refer to all areas at once in the if condition, to avoid doing in_area1 || in_area2 || ... || in_area20?)

However, if I add enough areas/points to represent my usecase (e.g. 2000 areas/points), I get a StackOverflowException. This is on a locally run GH instance, using the POST /route.

2025-07-04 12:46:57.279 [pool-2-thread-10 - POST /route?key=] ERROR i.d.j.errors.LoggingExceptionMapper - Error handling a request: 9fff1654956773e2
java.lang.StackOverflowError: null
        at com.graphhopper.routing.weighting.custom.ConditionalExpressionVisitor.visitRvalue(ConditionalExpressionVisitor.java:32)
        at org.codehaus.janino.Java$Rvalue.accept(Java.java:4495)

So it doesn’t seem to scale using this approach, which is a pity, because avoiding these points works like I would want it to, as a Proof of Concept :thinking:

Is there a way to save the points/polygons directly on the GH server (similar to how the GH server has the OpenStreetMap PBF data) and then refer to them as a certain type/category in the if condition of the request?

The “Request-URI Too Large” error should not happen as the /route endpoint with custom_model is only POST request. (probably you tried GH Maps? Then this error won’t occur in production)

Yes, the 414: “Request-URI Too Large” error appears when I use GH Maps :+1:

Currently there is no support for so many areas as this is not the use case. If you have so many locations likely the preferred way would be to block this on the server side via blocking access.

I get a StackOverflowException

Could be a limitation of janino. You could try to increase the allowed stack calls of the JVM.

Is there a way to save the points directly on the GH server

Currently not out of the box. You could modify the PBF and include access restrictions directly.

I tried increasing the stack size via -Xss512M when running GH and it does not throw a StackOverflowException anymore, instead it logs this exception as info (and also sends it as body of a 400 Bad Request response):

2025-07-04 16:14:32.741 [pool-2-thread-17 - POST /route?key=] INFO c.g.http.MultiExceptionMapper - bad request: [java.lang.IllegalArgumentException: Cannot compile expression: Compiling "JaninoCustomWeightingHelperSubclass3" in File 'source', Line 15, Column 8: File 'source', Line 2019, Column 16: Compiling "init(CustomModel customModel, EncodedValueLookup lookup, Map<String, com.graphhopper.util.JsonFeature> areas)"]

Again something to do with Janino. Are there some other configurations I need to set alongside the stack size?

Currently not out of the box. You could modify the PBF and include access restrictions directly.

I think if I modify the PBF by including the points or polygons, they would not be accessible when planning routes with GH, right? Since route planning only looks at road attributes (besides blocked areas that are passed along with the request). And if I modify the road segments/edges near points in the PBF, the blocking is static and permanent, not dynamic anymore :thinking:

Or is there a way I could refer to polygons that are already included in the PBF as blocked areas when planning routes? I am guessing that even if this was possible, it might lead to the same errors as above, when passing those same blocked areas along with the request?

This error message sounds strange. But what you try (thousands of exclusion areas) is out of spec at the moment and not really tested.

Instead I recommend you change the access after the import via the Java API directly.

I think if I modify the PBF by including the points or polygons, they would not be accessible when planning routes with GH, right?

You would need to modify existing ways (depending on the polygons) and add (access) tags based on which vehicles it should affect.

Another way could be to modify the access tags via the low level Java API (location index to find the edges to change the encoded values)

Yes, you can use a feature called ‘custom areas’. This is how GraphHopper sets the country encoded value as well. You can use a geojson file with all your areas and for each edge the matching areas will be available in the tag parsers. This way you could either block access to these edges in the access parser or create your own encoded value that you can use in your custom model (either on the client or server side).

2 Likes

Anything you do in a client custom model that you pass along with the request you can also do with a server-side custom model you setup in your configuration file.

We did some work towards using OSM relations to set road attributes for roads located within the area of relations here: Add landuse encoded value based on closed-ring ways tagged with landuse=* by easbar ¡ Pull Request #2765 ¡ graphhopper/graphhopper ¡ GitHub. But for the use case you are describing I think using the custom area feature, or the using the location index like karussell suggested would be ideal.

Hello @easbar!

Thank you both for the suggestions :slight_smile:

I have now created an custom_areas.geojson file with all 226822 points as square polygons, saved in 644 MultiPolygon areas with IDs. The structure looks like this:

{
  "type": "FeatureCollection",
  "features": [
    {
	  "id": "area1",
      "type": "Feature",
      "geometry": {
		"type": "MultiPolygon",
        "coordinates": [[
			[
			  [
				16.377413176051093,
				48.24704497473225
			  ],
			  [
				16.37768297322466,
				48.24704497473225
			  ],
			  [
				16.37768297322466,
				48.247224637789074
			  ],
			  [
				16.377413176051093,
				48.247224637789074
			  ],
			  [
				16.377413176051093,
				48.24704497473225
			  ]
			],
			[
			  [
			    16.47512672251574,
			    48.23899983460768
			  ],
			  [
			    16.475396477258208,
			    48.23899983460768
			  ],
			  [
			    16.475396477258208,
			    48.23917949766451
			  ],
			  [
			    16.47512672251574,
			    48.23917949766451
			  ],
			  [
			    16.47512672251574,
			    48.23899983460768
			  ]
		    ],
		    ...
		]]
      }
    },
    {
	  "id": "area2",
      "type": "Feature",
      "geometry": {
		"type": "MultiPolygon",
        "coordinates": [[
          [
            [
              16.474311019585684,
              48.2393951680767
            ],
            [
              16.474580776412758,
              48.2393951680767
            ],
            [
              16.474580776412758,
              48.239574831133524
            ],
            [
              16.474311019585684,
              48.239574831133524
            ],
            [
              16.474311019585684,
              48.2393951680767
            ]
          ],
          [
            [
              16.47483251595713,
              48.23917987318572
            ],
            [
              16.47510227164894,
              48.23917987318572
            ],
            [
              16.47510227164894,
              48.239359536242546
            ],
            [
              16.47483251595713,
              48.239359536242546
            ],
            [
              16.47483251595713,
              48.23917987318572
            ]
          ],
          [
            [
              16.474431104903747,
              48.23934493174089
            ],
            [
              16.474700861465916,
              48.23934493174089
            ],
            [
              16.474700861465916,
              48.239524594797714
            ],
            [
              16.474431104903747,
              48.239524594797714
            ],
            [
              16.474431104903747,
              48.23934493174089
            ]
          ],
          ...
        ]]
      }
    },
    ...
  ]
}

I have loaded the file via the custom_areas.directory in config.yml, which seems to work, as it prints INFO com.graphhopper.GraphHopper - Will make 644 areas available to all custom profiles. Found in custom_areas.

To test whether it would allow me to avoid areas in the same way as before (when passing some points as indiviual areas in the request), I extended the car profile in the same config file:

profiles:
   - name: car
     custom_model: 
      {
       "distance_influence": 90,
       "priority": [
         { "if": "!car_access", "multiply_by": "0" },
         { "if": "road_access == DESTINATION || road_access == PRIVATE", "multiply_by": "0.1" },
         {
          "if": "in_area1 || in_area2 || in_area3",
          "multiply_by": "0"
         }
       ],
       "speed": [
        { "if": "true", "limit_to": "car_average_speed" }
       ]
      }

But this does not seem to work. When using the car profile in GH maps to plan a route, it uses streets which should be blocked by the custom areas. It does recognize the areas though, as when I write an invalid area in the if condition (e.g. areaX), the server doesn’t start (Error: Cannot compile expression: Area 'areaX' wasn't found).

Also, the custom areas don’t appear on GH maps like they would when specifying them within the request (as orange squares). Is there a way to visualize those imported areas in GH Maps, to check if the import worked correctly? Is them not showing up a sign that something is not working? :thinking:

Currently this is not implemented, no.

I just tried this myself and it worked as expected. Maybe try a single, larger area to be sure?

But actually this is not the way I recommend approaching this. I would add a custom tag parser and encoded value (you need to modify Java code for this). Then in the handleWayTags method you will implement for the tag parser you can call way.getTag("custom_areas", null) which will give you all the custom areas containing the current edge. Here you will be able to set the encoded value depending on these custom areas, and finally you can use this encoded value in the custom model. This will also allow you to use a dedicated value for subsets of your areas like, e.g. areas 3, 6 and 7, so you won’t need to use in_area3 || in_area6 | in_area7 in the custom model (not sure what your exact requirements are).

Also note that you can click 'Show Routing Graph` in the layers menu in GraphHopper Maps and hover the edges to see all their encoded values.

I have 227.000 points of interests in 644 different categories, scattered across the city. The points are now saved in 644 different areas, representing the different categories. Each area contains one MultiPolygon that contains its many points. Each point is saved as a Polygon, a small square drawn around each point.

I cannot refer to a dedicated value for the intersection of different categories (e.g. areas 3, 6 and 7), since each edge visits a random combination of 644 potential areas. From my current knowledge, GraphHopper’s EncodedValue system only accepts fixed length types like String, Boolean, Integer or Enum, not variable length types like lists - Is this correct, and is this a limitation of the underlying OSM format? Even if I tried to implement a BitSet to represent the combinations, it would be too big for a LongEncodedValue :thinking:

I could save each intersected area in a custom StringEncodedValue (e.g. "area1;area7;area11;..."). The custom EncodedValue would need to be specified with the same large fixed length for each edge, to accommodate for the edge with the largest number of different areas, which seems a bit wasteful (and it would need to be manually increased if even more POI come in the future), right? Also, I don’t think I would be able to check whether my EncodedValue even contains the specific areas, since the operators in the custom model for custom fields don’t have contains/like/regex, etc.

But the in_ operator for custom areas offers this check - So from my current understanding, this is the only way to implement this usecase? :sweat_smile: But I would be happy to be corrected :slight_smile:

Yes, I found out why it didn’t work! I looked at the structure of countries.geojson again - the commas splitting the polygons in my custom_areas.geojson were wrongly set - they need to be on the top-level of the coordinates array, I had them one level deeper. This is the correct structure (pretty-printed this time):

{
	"type": "FeatureCollection",
    "features": [
        {
			"id": "area0",
            "type": "Feature",
            "geometry": {
                "type": "MultiPolygon",
                "coordinates": [
                    [
                        [
                            [
                                16.296075948059581,
                                48.173040639471267
                            ],
                            [
                                16.296345355627516,
                                48.173040639471267
                            ],
                            [
                                16.296345355627516,
                                48.173220302528094
                            ],
                            [
                                16.296075948059581,
                                48.173220302528094
                            ],
                            [
                                16.296075948059581,
                                48.173040639471267
                            ]
                        ]
                    ],
                    [
                        [
                            [
                                16.403048148807208,
                                48.145954561497547
                            ],
                            [
                                16.403317414170239,
                                48.145954561497547
                            ],
                            [
                                16.403317414170239,
                                48.146134224554373
                            ],
                            [
                                16.403048148807208,
                                48.146134224554373
                            ],
                            [
                                16.403048148807208,
                                48.145954561497547
                            ]
                        ]
                    ],
                    [
                        [
                            [
                                16.383575399774738,
                                48.187456406515288
                            ],
                            [
                                16.383844883112868,
                                48.187456406515288
                            ],
                            [
                                16.383844883112868,
                                48.187636069572115
                            ],
                            [
                                16.383575399774738,
                                48.187636069572115
                            ],
                            [
                                16.383575399774738,
                                48.187456406515288
                            ]
                        ]
                    ],
                    ...
                ]
            },
        },
        {
			"id": "area1",
            "type": "Feature",
            "geometry": {
                "type": "MultiPolygon",
                "coordinates": [
                    [
                        [
                            [
                                16.277239187030098,
                                48.20046689813045
                            ],
                            [
                                16.277508738803462,
                                48.20046689813045
                            ],
                            [
                                16.277508738803462,
                                48.200646561187277
                            ],
                            [
                                16.277239187030098,
                                48.200646561187277
                            ],
                            [
                                16.277239187030098,
                                48.20046689813045
                            ]
                        ]
                    ],
                    [
                        [
                            [
                                16.401103734873278,
                                48.173450754577345
                            ],
                            [
                                16.401373144595983,
                                48.173450754577345
                            ],
                            [
                                16.401373144595983,
                                48.173630417634172
                            ],
                            [
                                16.401103734873278,
                                48.173630417634172
                            ],
                            [
                                16.401103734873278,
                                48.173450754577345
                            ]
                        ]
                    ]
                ]
            },
        },
        ...
	]
}

It now works somewhat :grin:

It is easier to specify POI to avoid, as I can just reduce the priority of the intersecting edges, which are then avoided, if the pathfinding happens to come across them.

But even if I increase the priority of edges with POI I want to visit, they are lost in the bigger picture and mostly get ignored by the pathfinding, which goes for the fastest route. So I had the idea to draw even bigger squares around the POI, which intersect more edges in the area, as an “attraction zone”, that pulls the pathfinding towards it, should it happen to be nearby. The original smaller square in the middle still has a higher priority than the attraction zone, to hopefully pull the pathfinding towards the edge with the POI in the center, once it enters the attraction zone. The attraction zones I specified have the same name as their smaller center areas, but are prefixed with max (maxarea1, maxarea2, etc). Here is an example of the custom model I currently use:

{
    "points": [
        [
            16.368255615234375,
            48.23605047981043
        ],
        [
            16.4095401763916,
            48.180108968513231
        ]
    ],
    "profile": "car",
    "ch.disable": true,
    "custom_model": {
        "priority": [
            {
                "if": "!in_maxarea1 && !in_maxarea4 && !in_maxarea6",
                "multiply_by": "0.1"
            },
            {
                "if": "in_area1",
                "multiply_by": "1.8"
            },
            {
                "if": "in_area4",
                "multiply_by": "1.8"
            },
            {
                "if": "in_area6",
                "multiply_by": "1.8"
            },
            {
                "if": "in_area9 || in_area11 || in_area16",
                "multiply_by": "0.3"
            }
        ]
    }
}

This example shows a combination of POI to visit and POI to avoid. area1, area4 and area6 should be visited and area9, area11 and area16 should be avoided.

At first, for POI to visit, all edges outside of attraction zones are deprioritized, to increase the priority/pull of the attraction zones. Then the priority of each inner core area is increased individually. My idea was that since it is a multiplying factor, several POI occurring along one edge would rank it even higher - provided that each priority condition is evaluated one after the other, for each edge - Is this assumption correct?

For POI to avoid, I deprioritize the regular areas 9, 11 and 16, since those smaller, core areas are the ones that actually intersect the POI (unlike the larger max attraction zones).

Hopefully this is helpful for people who have a similar usecase in the future :+1:

Now while this does seem to work somewhat, I chose the priorities quite randomly. Do you have suggestions on how to find the correct value for the priorities, to balance the calculation? What is the upper threshold for priority, how far can/should the multiplying factors take it? I’m also open to any other suggestions.

Thanks :slight_smile:

Updated custom model with priorities < 1:

{
    "points": [
        [
            16.368255615234375,
            48.23605047981043
        ],
        [
            16.4095401763916,
            48.180108968513231
        ]
    ],
    "profile": "car",
    "ch.disable": true,
    "custom_model": {
        "priority": [
            {
                "if": "!in_maxarea1 && !in_maxarea4 && !in_maxarea6",
                "multiply_by": "0.1"
            },
            {
                "if": "!in_area1",
                "multiply_by": "0.8"
            },
            {
                "if": "!in_area4",
                "multiply_by": "0.8"
            },
            {
                "if": "!in_area6",
                "multiply_by": "0.8"
            },
            {
                "if": "in_area9 || in_area11 || in_area16",
                "multiply_by": "0.01"
            }
        ]
    }
}

Also, I thought about it more - The only way to implement this via tags would be to give edges individual boolean tags for all POI categories (e.g. visits_poi_category_1, visits_poi_category_2, etc) and set them accordingly during initialization. But from my understanding, even if the default value (false) would be sufficient for almost all edges, the memory space to hold the value would still reserved/used for each individual edge, so adding 650 tags would use a lot of memory.

I also realized that trying to use a BitSet/Mask in an encoded value wouldn’t work out of the box, just like a String, since the custom model also doesn’t have bitwise or modulo operators.

Hi @Tiefi

I am a bit late to the party but

Another option you could explore is to create your own custom weighting that changes the weight of the edges at run time during routing rather than via the profiles , this way you can have fine grained control of the weighting with no upper limit

class Custom(superWeighting: Weighting, hopper: GraphHopper) : AbstractAdjustedWeighting(superWeighting) {

    private fun shouldAvoidEdge(edgeId: Boolean): Boolean {
        // TODO Decide if edge should be avoided
    }

    override fun calcEdgeWeight(edgeState: EdgeIteratorState, reverse: Boolean): Double {
        
        return if (avoidIds.contains(edgeState.edge)) {
           edgePenaltyFactor
        } else {
            superWeighting.calcEdgeWeight(edgeState, reverse)
        }
    }

    override fun getName(): String? {
        return "avoidEdges"
    }
}

Then you just use this when routing, not you will have to create your own instance of router, you can’t just use hopper.route anymore

val weightingFactory = object : DefaultWeightingFactory(hopper.baseGraph, hopper.encodingManager) {
    override fun createWeighting(profile: Profile?, requestHints: PMap?, disableTurnCosts: Boolean): Weighting? {
        val weighting = super.createWeighting(profile, requestHints, disableTurnCosts)
        return AvoidEdgesWeighting(weighting, avoidedEdges, hopper)
    }
}


val profileMap = mutableMapOf<String, Profile>()
hopper.profiles.forEach { profileMap[it.name] = it }
val router =Router(
    hopper.baseGraph,
    hopper.encodingManager,
    hopper.locationIndex,
    profileMap,
    hopper.pathDetailsBuilderFactory,
    hopper.translationMap,
    hopper.routerConfig,
    weightingFactory, 
    hopper.chGraphs,
    hopper.landmarks)

Then you can just route using

router.route(request)

In order to precompute the edge Ids that need to be avoided I would take a look at the https://mvnrepository.com/artifact/org.locationtech.jts library, it has the ability to index polygons and check if your edge falls into the polygons