Index: doc/options.txt
===================================================================
--- doc/options.txt	(revision 3631)
+++ doc/options.txt	(working copy)
@@ -167,7 +167,6 @@
 in the same tile are used only. So the results close to a tile 
 border have less quality.  
 
-
 ;--housenumbers
 : Enables house number search for OSM input files. 
 
@@ -430,7 +429,6 @@
 problems. If MinLength is specified (in metres), arcs shorter
 than that length will be removed. If a length is not
 specified, only zero-length arcs will be removed.
- 
 
 ;--adjust-turn-headings[=BITMASK]
 : 	Where possible, ensure that turns off to side roads change
@@ -444,7 +442,6 @@
 :* 1 = increase angle between side road and outgoing main road
 :* 2 = increase angle between side road and incoming main road
 
-
 ;--report-similar-arcs
 : 	Issue a warning when more than one arc connects two nodes and
 the ways that the arcs are derived from contain identical
@@ -459,12 +456,11 @@
 :* 1 = multiple one-way roads that join together but go nowhere
 :* 2 = individual one-way roads that go nowhere.
 
-
 ;--road-name-pois[=GarminCode]
 :   Now ignored, former meaning:   	
 		Generate a POI for each named road. By default, the POIs'
 Garmin type code is 0x640a. If desired, a different type code
-can be specified with this option.  This s a workaround for not
+can be specified with this option.  This was a workaround for not
 being able to search for roads.
 
 : 0x2f15: a blue dot in the middle of the road, and if you select,
@@ -480,7 +476,6 @@
 :* end    - The last point of the line
 :* inner  - Each point of the line except the first and the last 
 :* mid    - The middle point
-:
 
 ;--add-pois-to-areas
 : 	Generate a POI for each polygon and multipolygon. The POIs are created 
@@ -494,13 +489,11 @@
 :: First rule that applies:
 ::* the first node tagged with a tag defined by the pois-to-areas-placement option
 ::* the centre point  
-::
 
 :;multipolygons:
 :: First rule that applies:
 ::* the node with role=label
 ::* the centre point of the biggest area
-:
 
 ;--pois-to-areas-placement[=taglist]
 :     A semicolon separated list of tag=value definitions. A POI is placed at the first
@@ -512,6 +505,44 @@
 
 : Default: entrance=main;entrance=yes;building=entrance
 
+;--nearby-poi-rules=type[/all|named|unnamed]:max-distance[:delete-poi|delete-name|merge-at-mid-point][,...]
+: 	Defines a set of rules to follow when a POI is near to another of the same type.
+Each rule consists of three parts separated by colons. The first two parts must be provided;
+the last part can be defaulted.
+
+The first part of the rule is the Garmin POI type code with an optional suffix; it determines when
+the rule is triggered. The type code may be specified in decimal or hexadecimal (e.g. 0x2c0b).
+A rule is triggered when processing a POI if the type code of the POI matches the rule type,
+providing there is also a match in the POI name and the first part suffix. If the suffix is '/all'
+(the default) then the match is only made on the type. If the suffix is '/named' then the rule is
+only triggered if the POI has a name. If the suffix is '/unnamed' then the rule is only triggered
+if the POI has no name. A wildcard of an asterisk character may be used to match any type code.
+The wildcard may also be combined with a suffix to allow separate processing of named and unnamed
+POIs.
+
+The second part of the rule is the distance in metres which an already processed POI must be within
+for it to be considered to be nearby and hence trigger the action part of the rule.
+
+The third part of the rule is the action part and provides three options:
+delete-poi - the POIS are considered to be duplicates and the duplicate is deleted
+delete-name - the POIS are not duplicates, but only a single name needs to be displayed
+merge-at-mid-point - the POIS are considered to be duplicates but the location of the existing point
+is moved to a point midway between the two points.
+
+Rules continue to be applied until either the point is deleted or there are no more applicable rules.
+Wildcard rules are only applied if no other rule is applicable.
+
+For example:
+--nearby-poi-rules=*/named:50,*/unnamed:25,0x2f1f/named:50:delete-name,0x2f1f:3,0x641d:400:merge-at-mid-point
+This has the following effect:
+If no other rule applies, a POI with the same name and type and within 50m of one already processed will be deleted
+If no other rule applies, a POI with no name and type and within 25m of one already processed will be deleted
+A POI of type 0x2f1f that is within 50m of another POI with the same name and type will have its name deleted
+A POI of type 0x2f1f that is within 3m of another POI with the same type will be deleted
+A POI of type 0x641d that is within 400m of another POI with the same type will be deleted and the existing point moved
+
+Note: a POI that matches another in type, name and exact location is always considered a duplicate and deleted.
+
 ;--precomp-sea=directory|zipfile
 :     Defines the directory or a zip file that contains precompiled sea tiles. 
 Sea files in a zip file must be located in the zip files root directory or in 
@@ -576,7 +607,6 @@
 :: switches on the debugging of the flood blocker
 generates GPX files for each polygon checked by
 the flood blocker
-:
 
 ;--make-poi-index
 : 	Generate the POI index (not yet useful).
Index: resources/help/en/options
===================================================================
--- resources/help/en/options	(revision 3631)
+++ resources/help/en/options	(working copy)
@@ -296,18 +296,18 @@
   Area name is displayed on Garmin units (or at least on eTrex) as the second 
   part of the mapname in the list of the individual maps.
    
---copyright-message=note
-	Specify a copyright message for files that do not contain one.
+--copyright-message=note
+	Specify a copyright message for files that do not contain one.
+
+--copyright-file=file
+	Specify copyright messages from a file.
+	Note that the first copyright message is not displayed on a device, but is 
+	shown in BaseCamp. The copyright file must include at least two lines.
+
+--license-file=file
+	Specify a file which content will be added as license. 
+  All entries of all maps will be merged in the overview map.
 
---copyright-file=file
-	Specify copyright messages from a file.
-	Note that the first copyright message is not displayed on a device, but is 
-	shown in BaseCamp. The copyright file must include at least two lines.
-
---license-file=file
-	Specify a file which content will be added as license. 
-  All entries of all maps will be merged in the overview map.
-
 Optimization options:
 
 --reduce-point-density=NUM
@@ -506,6 +506,44 @@
     It is possible to define wildcards for tag values like entrance=*.
     Default: entrance=main;entrance=yes;building=entrance
 
+--nearby-poi-rules=type[/all|named|unnamed]:max-distance[:delete-poi|delete-name|merge-at-mid-point][,...]
+ 	Defines a set of rules to follow when a POI is near to another of the same type.
+	Each rule consists of three parts separated by colons. The first two parts must be provided;
+	the last part can be defaulted.
+
+	The first part of the rule is the Garmin POI type code with an optional suffix; it determines when
+	the rule is triggered. The type code may be specified in decimal or hexadecimal (e.g. 0x2c0b).
+	A rule is triggered when processing a POI if the type code of the POI matches the rule type,
+	providing there is also a match in the POI name and the first part suffix. If the suffix is '/all'
+	(the default) then the match is only made on the type. If the suffix is '/named' then the rule is
+	only triggered if the POI has a name. If the suffix is '/unnamed' then the rule is only triggered
+	if the POI has no name. A wildcard of an asterisk character may be used to match any type code.
+	The wildcard may also be combined with a suffix to allow separate processing of named and unnamed
+	POIs.
+
+	The second part of the rule is the distance in metres which an already processed POI must be within
+	for it to be considered to be nearby and hence trigger the action part of the rule.
+
+	The third part of the rule is the action part and provides three options:
+	delete-poi - the POIS are considered to be duplicates and the duplicate is deleted
+	delete-name - the POIS are not duplicates, but only a single name needs to be displayed
+	merge-at-mid-point - the POIS are considered to be duplicates but the location of the existing point
+	is moved to a point midway between the two points.
+
+	Rules continue to be applied until either the point is deleted or there are no more applicable rules.
+	Wildcard rules are only applied if no other rule is applicable.
+
+	For example:
+		--nearby-poi-rules=*/named:50,*/unnamed:25,0x2f1f/named:50:delete-name,0x2f1f:3,0x641d:400:merge-at-mid-point
+	This has the following effect:
+		If no other rule applies, a POI with the same name and type and within 50m of one already processed will be deleted
+		If no other rule applies, a POI with no name and type and within 25m of one already processed will be deleted
+		A POI of type 0x2f1f that is within 50m of another POI with the same name and type will have its name deleted
+		A POI of type 0x2f1f that is within 3m of another POI with the same type will be deleted
+		A POI of type 0x641d that is within 400m of another POI with the same type will be deleted and the existing point moved
+
+	Note: a POI that matches another in type, name and exact location is always considered a duplicate and deleted.
+
 --precomp-sea=directory|zipfile
     Defines the directory or a zip file that contains precompiled sea tiles. 
     Sea files in a zip file must be located in the zip file's root directory or in 
Index: src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java
===================================================================
--- src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java	(revision 3631)
+++ src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java	(working copy)
@@ -145,7 +145,18 @@
 	private final boolean mergeRoads;
 	private final boolean routable;
 	
-
+	private enum NearbyPoiActOn {ActOnAll,ActOnNamed,ActOnUnnamed};
+	private enum NearbyPoiAction {DeletePOI,DeleteName,MergeAtMidPoint};
+	private class NearbyPoiRule {
+		public int poiType = 0;
+		public int maxDistance = 0;
+		public NearbyPoiActOn actOn = NearbyPoiActOn.ActOnAll;
+		public NearbyPoiAction action = NearbyPoiAction.DeletePOI;
+	}
+	private ArrayList<NearbyPoiRule> nearbyPoiRules = new ArrayList<>();
+	private NearbyPoiRule defaultNamedNearbyPoiRule = new NearbyPoiRule();
+	private NearbyPoiRule defaultUnnamedNearbyPoiRule = new NearbyPoiRule();
+	
 	private LineAdder lineAdder = new LineAdder() {
 		public void add(MapLine element) {
 			if (element instanceof MapRoad){
@@ -204,10 +215,105 @@
 			lineAdder = overlayAdder;
 		linkPOIsToWays = props.getProperty("link-pois-to-ways", false);
 		
+		defaultNamedNearbyPoiRule.actOn = NearbyPoiActOn.ActOnNamed;
+		defaultUnnamedNearbyPoiRule.actOn = NearbyPoiActOn.ActOnUnnamed;
+		String [] rules = props.getProperty("nearby-poi-rules","").split(",");
+		for (int i = 0; i < rules.length; i++) {
+			String rule = rules[i];
+			if (!rule.isEmpty()) {
+				NearbyPoiRule nearbyPoiRule = new NearbyPoiRule();
+				Boolean valid = true;
+				String [] ruleParts = rule.split(":", 4);
+				String part1 = ruleParts[0];
+				int slashPos = part1.indexOf("/");
+				if (slashPos > 0) {
+					String part1Suffix = part1.substring(slashPos + 1);
+					part1 = part1.substring(0, slashPos);
+					switch (part1Suffix.toLowerCase()) {
+						case "named":
+							nearbyPoiRule.actOn = NearbyPoiActOn.ActOnNamed;
+							break;
+							
+						case "unnamed":
+							nearbyPoiRule.actOn = NearbyPoiActOn.ActOnUnnamed;
+							break;
+						
+						case "all":
+							break;
+						
+						default:
+							valid = false;
+							log.error("Invalid Act On value",part1Suffix,"in --nearby-poi-rules option - 'all', 'named' or 'unnamed' expected.");
+							break;
+					}
+				}
+				if (!part1.equals("*")) {
+					try {
+						nearbyPoiRule.poiType = Integer.decode(part1);
+					}
+					catch (Exception ex) {
+						valid = false;
+						log.error("Invalid POI type",part1,"in --nearby-poi-rules option."); 
+					}
+				}
+				if (ruleParts.length > 1) {
+					try {
+						nearbyPoiRule.maxDistance = Integer.parseInt(ruleParts[1]);
+					}
+					catch (Exception ex) {
+						valid = false;
+						log.error("Invalid maximum distance",ruleParts[1],"in --nearby-poi-rules option."); 
+					}
+					if (ruleParts.length > 2) {
+						switch (ruleParts[2].toLowerCase()) {
+							case "delete-poi":
+								break;
+								
+							case "delete-name":
+								nearbyPoiRule.action = NearbyPoiAction.DeleteName;
+								break;
+								
+							case "merge-at-mid-point":
+								nearbyPoiRule.action = NearbyPoiAction.MergeAtMidPoint;
+								break;
+
+							default:
+								valid = false;
+								log.error("Invalid Action value",ruleParts[2],"in --nearby-poi-rules option - 'delete-poi', 'delete-name' or'merge-at-mid-point' expected."); 
+								break;
+						}
+					}
+					if (ruleParts.length > 3)
+						log.warn("Unexpected text",ruleParts[3],"in --nearby-poi-rules option.");
+					if (valid) {
+						if (part1.equals("*")) {
+							if (nearbyPoiRule.actOn != NearbyPoiActOn.ActOnUnnamed)
+								defaultNamedNearbyPoiRule = nearbyPoiRule;
+							if (nearbyPoiRule.actOn != NearbyPoiActOn.ActOnNamed)
+								defaultUnnamedNearbyPoiRule = nearbyPoiRule;
+						}
+						else {
+							int index;
+							int count = nearbyPoiRules.size();
+							for (index = 0; index < count; index++) {
+								if (nearbyPoiRule.poiType < nearbyPoiRules.get(index).poiType) {
+									nearbyPoiRules.add(index, nearbyPoiRule);
+									break;
+								}
+							}
+							if (index == count)
+								nearbyPoiRules.add(nearbyPoiRule);
+						}
+					}
+				}
+				else
+					log.error("Maximum distance expected for POI type",part1,"in --nearby-poi-rules option.");
+			}
+		}
+
 		// undocumented option - usually used for debugging only
 		mergeRoads = props.getProperty("no-mergeroads", false) == false;
 		routable = props.containsKey("route");
-		
 	}
 
 	/** One type result for ways to avoid recreating one for each way. */ 
@@ -1027,9 +1133,9 @@
 		if (dupPOI){
 			if (log.isInfoEnabled()){
 				if (FakeIdGenerator.isFakeId(node.getId()))
-					log.info("ignmoring duplicate POI with type",GType.formatType(type),mp.getName(),"for generated element with id",node.getId(),"at",mp.getLocation().toDegreeString());
+					log.info("ignoring duplicate POI with type",GType.formatType(type),mp.getName(),"for element generated from original id",node.getOriginalId(),"at",mp.getLocation().toDegreeString());
 				else 
-					log.info("ignmoring duplicate POI with type",GType.formatType(type),mp.getName(),"for element",node.toBrowseURL());
+					log.info("ignoring duplicate POI with type",GType.formatType(type),mp.getName(),"for element",node.toBrowseURL());
 			}
 			return;
 		}
@@ -1043,17 +1149,70 @@
 	 * @return
 	 */
 	private boolean checkDuplicatePOI(MapPoint mp) {
-		Map<String, MapPoint> typeMap = pointMap.get(mp.getType());
+		int type = mp.getType();
+		Map<String, MapPoint> typeMap = pointMap.get(type);
 		if (typeMap == null){
 			typeMap = new HashMap<>();
 			pointMap.put(mp.getType(), typeMap);
 		}
-		MapPoint old = typeMap.get(mp.getName());
+		String name = mp.getName();
+		MapPoint old = typeMap.get(name);
 		if (old == null){
 			typeMap.put(mp.getName(), mp);
 		} else {
-			if (old.getLocation().equals(mp.getLocation()))
-				return true;
+			Coord location = mp.getLocation();
+			Coord oldLocation = old.getLocation();
+			if ((location != null) && (oldLocation != null)) {
+				if (oldLocation.equals(location))
+					return true;
+				
+				boolean ruleApplied = false;
+				for (NearbyPoiRule rule : nearbyPoiRules) {
+					if (rule.poiType > type)
+						break; // rules are in ascending order of poiType, so we can stop trying other rules
+
+					if ((rule.poiType == type) &&
+						((rule.actOn == NearbyPoiActOn.ActOnAll) ||
+						 ((rule.actOn == NearbyPoiActOn.ActOnNamed) && (name != null) && !name.isEmpty()) ||
+						 ((rule.actOn == NearbyPoiActOn.ActOnUnnamed) && ((name == null) || name.isEmpty())))) {
+						if (location.distance(oldLocation) > rule.maxDistance)
+							ruleApplied = true;
+						else {
+							switch (rule.action) {
+								case DeletePOI:
+									return true;
+									
+								case DeleteName:
+									mp.setName("");
+									ruleApplied = true;
+									break;
+
+								case MergeAtMidPoint:
+									old.setLocation(location.makeBetweenPoint(oldLocation, 0.5));
+									return true;
+							}
+						}
+					}
+				}
+
+				if (!ruleApplied) {
+					NearbyPoiRule defaultRule = (name == null) ? defaultUnnamedNearbyPoiRule : defaultNamedNearbyPoiRule;
+					if ((location.distance(oldLocation) < defaultRule.maxDistance)) {
+						switch (defaultRule.action) {
+							case DeletePOI:
+								return true;
+								
+							case DeleteName:
+								mp.setName("");
+								return false;
+
+							case MergeAtMidPoint:
+								old.setLocation(location.makeBetweenPoint(oldLocation, 0.5));
+								return true;
+						}
+					}
+				}
+			}
 		}
 		return false;
 	}
