Index: src/uk/me/parabola/mkgmap/osmstyle/NearbyPoiHandler.java
===================================================================
--- src/uk/me/parabola/mkgmap/osmstyle/NearbyPoiHandler.java	(revision 4485)
+++ src/uk/me/parabola/mkgmap/osmstyle/NearbyPoiHandler.java	(working copy)
@@ -20,21 +20,27 @@
 import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import uk.me.parabola.imgfmt.app.Coord;
 import uk.me.parabola.log.Logger;
 import uk.me.parabola.mkgmap.general.MapPoint;
 import uk.me.parabola.mkgmap.reader.osm.FakeIdGenerator;
-import uk.me.parabola.mkgmap.reader.osm.GType;
 import uk.me.parabola.mkgmap.reader.osm.Node;
 import uk.me.parabola.util.EnhancedProperties;
+import uk.me.parabola.util.KdTree;
 
 public class NearbyPoiHandler {
 	private static final Logger log = Logger.getLogger(NearbyPoiHandler.class);
-	private final HashMap<Integer, Map<String, MapPoint>> pointMap = new HashMap<>();
 
 	private enum NearbyPoiActOn {
 		ActOnAll, ActOnNamed, ActOnUnnamed
@@ -186,80 +192,130 @@
 		}
 	}
 
-	private static boolean applyRuleAction(Node node, MapPoint mp, MapPoint old, NearbyPoiAction action) {
-		int type = mp.getType();
-		Coord location = mp.getLocation();
-		switch (action) {
-		case DeletePOI:
-			if (log.isInfoEnabled()) {
-				log.info("Removed nearby duplicate POI with type", GType.formatType(type), mp.getName(), getLogInfo(node));
+	private static String getLogInfo(Node n) {
+		return "for element " + (FakeIdGenerator.isFakeId(n.getId()) ? "generated from " + n.getOriginalId()
+				: n.toBrowseURL() + " at " + n.getLocation().toOSMURL());
+	}
+
+	public List<MapPoint> deDuplicate(List<MapPoint> allPOI) {
+		Set<MapPoint> toKeep = new HashSet<>();
+		Map<Integer, Map<String, List<MapPoint>>> byTypeAndName = allPOI.stream()
+                .collect(Collectors.groupingBy(MapPoint::getType,
+                		Collectors.groupingBy(mp -> mp.getName() == null ? "" : mp.getName(),
+                				Collectors.toList())));
+		for (Entry<Integer, Map<String, List<MapPoint>>> e : byTypeAndName.entrySet()) {
+			for (Entry<String, List<MapPoint>> e2 : e.getValue().entrySet()) {
+				toKeep.addAll(reduce(e2.getValue()));
 			}
-			return true;
+		}
+		if (toKeep.size() < allPOI.size())
+			return allPOI.stream().filter(toKeep::contains).collect(Collectors.toList());
+		else 
+			return allPOI;
+	}
 
-		case DeleteName:
-			if (log.isInfoEnabled()) {
-				log.info("Removed name from nearby duplicate POI with type", GType.formatType(type), mp.getName(), getLogInfo(node));
+	private Collection<MapPoint> reduce(List<MapPoint> points) {
+		if (points.size() == 1)
+			return points;
+		int type = points.get(0).getType();
+		String name = points.get(0).getName();
+		for (NearbyPoiRule rule : nearbyPoiRules) {
+			if (rule.minPoiType > type)
+				break; // rules are in ascending order of poiType, so we can
+						// stop trying other rules
+
+			if ((rule.minPoiType <= type) && (rule.maxPoiType >= type)
+					&& ((rule.actOn == NearbyPoiActOn.ActOnAll)
+							|| ((rule.actOn == NearbyPoiActOn.ActOnNamed) && (name != null) && !name.isEmpty())
+							|| ((rule.actOn == NearbyPoiActOn.ActOnUnnamed) && ((name == null) || name.isEmpty())))) {
+				return useRule(points, rule);
 			}
-			mp.setName("");
-			return false;
+		}
 
-		case MergeAtMidPoint:
-			Coord oldLocation = old.getLocation();
-			Coord newLocation = location.makeBetweenPoint(oldLocation, 0.5);
-			old.setLocation(newLocation);
-			if (log.isInfoEnabled()) {
-				log.info("Removed nearby duplicate POI with type", GType.formatType(type), mp.getName(),
-						getLogInfo(node), "and moved location of mearby POI from", oldLocation, "to", newLocation);
+		NearbyPoiRule defaultRule = (name == null) ? defaultUnnamedNearbyPoiRule : defaultNamedNearbyPoiRule;
+		return useRule(points, defaultRule);
+	}
+
+	private Collection<MapPoint> useRule(List<MapPoint> points, NearbyPoiRule rule) {
+		final KdTree<MapPoint> kdTree = new KdTree<>();
+		points.forEach(kdTree::add);
+		List<MapPoint> toKeep = new ArrayList<>();
+		Map<MapPoint, Set<MapPoint>> nearbyMap = new HashMap<>();
+		for (MapPoint mp : points) {
+			Set<MapPoint> set = kdTree.findClosePoints(mp, rule.maxDistance);
+			if (set.size() <= 1) {
+				toKeep.add(mp); // no other point is close 
+			} else if (set.size() == points.size()) {
+				return Collections.singleton(mp); // all points build one small cloud
+			} else {
+				// collect cloud
+				nearbyMap.put(mp, set);
 			}
-			return true;
 		}
-		return false; // should not happen
-	}
-	
-	public boolean isDuplicatePOI(Node node, MapPoint mp) {
-		int type = mp.getType();
-		Map<String, MapPoint> typeMap = pointMap.computeIfAbsent(type, HashMap::new);
-		String name = mp.getName();
-		MapPoint old = typeMap.get(name);
-		if (old == null)
-			typeMap.put(mp.getName(), mp);
-		else {
-			Coord location = mp.getLocation();
-			Coord oldLocation = old.getLocation();
-			if ((location != null) && (oldLocation != null)) {					
-				if (oldLocation.equals(location)) {
-					if (log.isInfoEnabled())
-						log.info("Removed duplicate POI with type", GType.formatType(type), mp.getName(), getLogInfo(node));
-					return true;
+		while (!nearbyMap.isEmpty()) {
+			Set<MapPoint> biggestCloud = null;
+			for (Entry<MapPoint, Set<MapPoint>> e : nearbyMap.entrySet()) {
+				Set<MapPoint> set = e.getValue();
+				if (biggestCloud == null || set.size() > biggestCloud.size()) {
+					biggestCloud = set;
 				}
-				
-				boolean ruleApplied = false;
-				for (NearbyPoiRule rule : nearbyPoiRules) {
-					if (rule.minPoiType > type)
-						break; // rules are in ascending order of poiType, so we can stop trying other rules
+			}
+			
+			// calculate center of cloud
+			double lat = 0;
+			double lon = 0;
+			for (MapPoint mp : biggestCloud) {
+				Coord p = mp.getLocation();
+				lat += (double) p.getHighPrecLat() / biggestCloud.size();
+				lon += (double) p.getHighPrecLon() / biggestCloud.size();
+			}
+			Coord middle = Coord.makeHighPrecCoord((int) Math.round(lat), (int) Math.round(lon));
 
-					if ((rule.minPoiType <= type) && (rule.maxPoiType >= type) &&
-						((rule.actOn == NearbyPoiActOn.ActOnAll) ||
-						 ((rule.actOn == NearbyPoiActOn.ActOnNamed) && (name != null) && !name.isEmpty()) ||
-						 ((rule.actOn == NearbyPoiActOn.ActOnUnnamed) && ((name == null) || name.isEmpty())))) {
-						ruleApplied = true;
-						if (location.distance(oldLocation) <= rule.maxDistance)
-							return applyRuleAction(node, mp, old, rule.action);
+			// select point that is closest to the middle
+			MapPoint bestPoint = biggestCloud.iterator().next();
+			double bestDist = Double.MAX_VALUE;
+			for (MapPoint mp : biggestCloud) {
+				double dist = mp.getLocation().distance(bestPoint.getLocation());
+				if (dist < bestDist) {
+					bestPoint = mp;
+					bestDist = dist;
+				}
+			}
+			
+			if (rule.action == NearbyPoiAction.DeleteName) {
+				for (MapPoint mp : biggestCloud) {
+					if (mp != bestPoint) {
+						mp.setName(null);
 					}
 				}
+			} else {
+				// this is the point that will be displayed
+				toKeep.add(bestPoint);
 
-				if (!ruleApplied) {
-					NearbyPoiRule defaultRule = (name == null) ? defaultUnnamedNearbyPoiRule : defaultNamedNearbyPoiRule;
-					if ((location.distance(oldLocation) < defaultRule.maxDistance))
-						return applyRuleAction(node, mp, old, defaultRule.action);
+				if (rule.action == NearbyPoiAction.MergeAtMidPoint) {
+					//TODO: maintain flags like SKIP_DEAD_END_CHECK_NODE_MASK
+					bestPoint.setLocation(middle);
 				}
 			}
+			
+			// remove the processed points
+			Iterator<Entry<MapPoint, Set<MapPoint>>> iter = nearbyMap.entrySet().iterator();
+			while (iter.hasNext()) {
+				Entry<MapPoint, Set<MapPoint>> e = iter.next();
+				if (e.getKey() == bestPoint) 
+					iter.remove();
+				else {
+					Set<MapPoint> set = e.getValue();
+					set.removeAll(biggestCloud);
+					if (set.isEmpty()) {
+						iter.remove();
+					}
+				}
+			}
 		}
-		return false;
+		if (rule.action != NearbyPoiAction.DeleteName)
+			return toKeep;
+		else 
+			return points;
 	}
-	
-	private static String getLogInfo(Node n) {
-		return "for element " + (FakeIdGenerator.isFakeId(n.getId()) ? "generated from " + n.getOriginalId()
-				: n.toBrowseURL() + " at " + n.getLocation().toOSMURL());
-	}
 }
Index: src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java
===================================================================
--- src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java	(revision 4485)
+++ src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java	(working copy)
@@ -31,6 +31,7 @@
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.logging.Level;
+import java.util.stream.Collectors;
 
 import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
 import uk.me.parabola.imgfmt.ExitException;
@@ -118,7 +119,6 @@
 	private IdentityHashMap<Coord, CoordNode> nodeIdMap = new IdentityHashMap<>();
 
 	public static final String WAY_POI_NODE_IDS = "mkgmap:way-poi-node-ids";
-	private final HashMap<Integer, Map<String, MapPoint>> pointMap;
 	
 	/** boundary ways with admin_level=2 */
 	private final Set<Way> borders = new LinkedHashSet<>();
@@ -125,10 +125,9 @@
 	private boolean addBoundaryNodesAtAdminBoundaries;
 	private int admLevelNod3;
 	
-	
+	private List<MapPoint> allPOI = new ArrayList<>();
 	private List<ConvertedWay> roads = new ArrayList<>();
 	private List<ConvertedWay> lines = new ArrayList<>();
-	private List<MapPoint> renderedCoordPOI = new ArrayList<>();
 
 	private int nextRoadId = 1;
 	
@@ -167,7 +166,6 @@
 
 		nameFinder = new NameFinder(props);
 		this.style = style;
-		pointMap = new HashMap<>();
 		wayRules = style.getWayRules();
 		nodeRules = style.getNodeRules();
 		lineRules = style.getLineRules();
@@ -575,14 +573,7 @@
 			}
 			elementSetup(mp, gt, node);
 			mp.setLocation(node.getLocation());
-
-			if (nearbyPoiHandler.isDuplicatePOI(node, mp))
-				return;
-
-			if (node.getLocation() instanceof CoordPOI) {
-				renderedCoordPOI.add(mp);
-			}
-			collector.addPoint(mp);
+			allPOI.add(mp);
 		}
 
 		/**
@@ -860,7 +851,6 @@
 	}
 	
 	public void end() {
-		pointMap.clear();
 		style.reportStats();
 		driveOnLeft = calcDrivingSide();
 		
@@ -875,8 +865,11 @@
 
 		HashSet<Long> deletedRoads = new HashSet<>();
 		WrongAngleFixer wrongAngleFixer = new WrongAngleFixer(bbox);
-		wrongAngleFixer.optimizeWays(roads, lines, modifiedRoads, deletedRoads, restrictions, renderedCoordPOI);
 		
+		wrongAngleFixer.optimizeWays(roads, lines, modifiedRoads, deletedRoads, restrictions, allPOI);
+		nearbyPoiHandler.deDuplicate(allPOI).forEach(collector::addPoint);
+		allPOI = null;
+		
 		// make sure that copies of modified roads have equal points 
 		for (ConvertedWay line : lines){
 			if (!line.isValid())
Index: src/uk/me/parabola/mkgmap/osmstyle/WrongAngleFixer.java
===================================================================
--- src/uk/me/parabola/mkgmap/osmstyle/WrongAngleFixer.java	(revision 4485)
+++ src/uk/me/parabola/mkgmap/osmstyle/WrongAngleFixer.java	(working copy)
@@ -125,11 +125,13 @@
 			}
 		}
 
-		// update positions of the POIs which were already created. 
+		// update positions of the POIs which were already created.
 		for (MapPoint mp : renderedPOI) {
-			Coord replacement = getReplacement(mp.getLocation(), null, replacements);
-			if (mp.getLocation() != replacement) {
-				mp.setLocation(replacement);
+			if (mp.getLocation() instanceof CoordPOI) {
+				Coord replacement = getReplacement(mp.getLocation(), null, replacements);
+				if (mp.getLocation() != replacement) {
+					mp.setLocation(replacement);
+				}
 			}
 		}
 	}	
