/*
 * Copyright (C) 2013.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

package uk.me.parabola.mkgmap.osmstyle.housenumber;

import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.net.Numbers;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.CityInfo;
import uk.me.parabola.mkgmap.general.LineAdder;
import uk.me.parabola.mkgmap.general.MapRoad;
import uk.me.parabola.mkgmap.general.ZipCodeInfo;
import uk.me.parabola.mkgmap.reader.osm.Element;
import uk.me.parabola.mkgmap.reader.osm.HousenumberHooks;
import uk.me.parabola.mkgmap.reader.osm.Node;
import uk.me.parabola.mkgmap.reader.osm.POIGeneratorHook;
import uk.me.parabola.mkgmap.reader.osm.Relation;
import uk.me.parabola.mkgmap.reader.osm.TagDict;
import uk.me.parabola.mkgmap.reader.osm.Way;
import uk.me.parabola.util.EnhancedProperties;
import uk.me.parabola.util.KdTree;
import uk.me.parabola.util.Locatable;
import uk.me.parabola.util.MultiHashMap;

/**
 * Collects all data required for OSM house number handling and adds the
 * house number information to the roads.
 * 
 * @author WanMil, Gerd Petermann
 */
public class HousenumberGenerator {
	private static final Logger log = Logger.getLogger(HousenumberGenerator.class);

	/** Gives the maximum distance between house number element and the matching road */
	public static final double MAX_DISTANCE_TO_ROAD = 150d;
	/** Gives the maximum distance for different elements with the same address */
	public static final double MAX_DISTANCE_SAME_NUM = 100d;
	
	private boolean numbersEnabled;

	// options for handling of unnamed (service?) roads	
	private int nameSearchDepth = 3;

	private MultiHashMap<String, HousenumberIvl> interpolationWays;
	private List<MapRoad> allRoads;
	private Map<Long,Integer> interpolationNodes;
	private List<HousenumberElem> houseElems;
	private HashMap<CityInfo, CityInfo> cityInfos = new HashMap<>();
	private HashMap<ZipCodeInfo, ZipCodeInfo> zipInfos = new HashMap<>();

	private static final short TKM_HOUSENUMBER =  TagDict.getInstance().xlate("mkgmap:housenumber");
	private static final short TK_ADDR_HOUSENUMBER =  TagDict.getInstance().xlate("addr:housenumber");
	private static final short TKM_STREET = TagDict.getInstance().xlate("mkgmap:street");
	private static final short TK_ADDR_STREET = TagDict.getInstance().xlate("addr:street");
	private static final short TK_ADDR_INTERPOLATION = TagDict.getInstance().xlate("addr:interpolation");
	private static final short TK_ADDR_PLACE = TagDict.getInstance().xlate("addr:place");
	private static final short TKM_CITY = TagDict.getInstance().xlate("mkgmap:city");
	private static final short TKM_REGION = TagDict.getInstance().xlate("mkgmap:region");
	private static final short TKM_COUNTRY = TagDict.getInstance().xlate("mkgmap:country");
	private static final short TKM_POSTAL_CODE = TagDict.getInstance().xlate("mkgmap:postal_code");
	private static final short TKM_NUMBERS = TagDict.getInstance().xlate("mkgmap:numbers");
	
	public HousenumberGenerator(EnhancedProperties props) {
		this.interpolationWays = new MultiHashMap<>();
		this.allRoads = new ArrayList<>();
		this.interpolationNodes = new HashMap<>();
		this.houseElems = new ArrayList<>();
		
		numbersEnabled = props.containsKey("housenumbers");
		int n = props.getProperty("name-service-roads", 3);
		if (n != nameSearchDepth) {
			nameSearchDepth = Math.min(25, Math.max(0, n));
			if (nameSearchDepth != n) {
				Logger.defaultLogger.warn("name-service-roads=" + n + " was changed to name-service-roads=" + nameSearchDepth);
			}
		}
	}

	/**
	 * Retrieves the street name of this element.
	 * @param e an OSM element
	 * @return the street name (or {@code null} if no street name set)
	 */
	private static String getStreetname(Element e) {
		String streetname = e.getTag(TKM_STREET);
		if (streetname == null) {
			streetname = e.getTag(TK_ADDR_STREET);
		}	
		return streetname;
	}
	
	/**
	 * Retrieves the house number of this element.
	 * @param e an OSM element
	 * @return the house number (or {@code null} if no house number set)
	 */
	public static String getHousenumber(Element e) {
		String res = e.getTag(TKM_HOUSENUMBER); 
		if (res != null)
			return res;
		return e.getTag(TK_ADDR_HOUSENUMBER);
	}
	
	/**
	 * Parses the house number string. It accepts the first positive number part
	 * of a string. So all leading and preceding non number parts are ignored.
	 * So the following strings are accepted:
	 * <table>
	 * <tr>
	 * <th>Input</th>
	 * <th>Output</th>
	 * </tr>
	 * <tr>
	 * <td>23</td>
	 * <td>23</td>
	 * </tr>
	 * <tr>
	 * <td>-23</td>
	 * <td>23</td>
	 * </tr>
	 * <tr>
	 * <td>21-23</td>
	 * <td>21</td>
	 * </tr>
	 * <tr>
	 * <td>Abc 21</td>
	 * <td>21</td>
	 * </tr>
	 * <tr>
	 * <td>Abc 21.45</td>
	 * <td>21</td>
	 * </tr>
	 * <tr>
	 * <td>21 Main Street</td>
	 * <td>21</td>
	 * </tr>
	 * <tr>
	 * <td>Main Street</td>
	 * <td><i>IllegalArgumentException</i></td>
	 * </tr>
	 * </table>
	 * @throws IllegalArgumentException if parsing fails
	 */
	private static Integer parseHousenumber(String housenumberString) {
		if (housenumberString == null) {
			return null;
		}
		
		// the housenumber must match against the pattern <anything>number<notnumber><anything>
		int housenumber;
		Pattern p = Pattern.compile("\\D*(\\d+)\\D?.*");
		Matcher m = p.matcher(housenumberString);
		if (!m.matches()) {
			return null;
		}
		try {
			// get the number part and parse it
			housenumber = Integer.parseInt(m.group(1));
		} catch (NumberFormatException exp) {
			return null;
		}
		return housenumber;
	}

	
	private HousenumberElem parseElement(Element el, String sign) {
		String city = el.getTag(TKM_CITY);
		String region = el.getTag(TKM_REGION);
		String country = el.getTag(TKM_COUNTRY);
		CityInfo ci = getCityInfos(city,region,country);
		HousenumberElem house = new HousenumberElem(el, ci);
		if (house.getLocation() == null) {
			// there has been a report that indicates match.getLocation() == null
			// could not reproduce so far but catching it here with some additional
			// information. (WanMil)
			log.error("OSM element seems to have no point.");
			log.error("Element: " + el.toBrowseURL() + " " + el);
			log.error("Please report on the mkgmap mailing list.");
			log.error("Continue creating the map. This should be possible without a problem.");
			return null;
		}
		
		house.setSign(sign);
		Integer hn = parseHousenumber(sign);
		if (hn == null) {
			if (log.isDebugEnabled())
				log.debug("No housenumber (", el.toBrowseURL(), "): ", sign);
			return null;
		}
		if (hn < 0 || hn > 1_000_000) {
			log.warn("Number looks wrong, is ignored",house.getSign(),hn,"element",el.toBrowseURL());
			return null;
		}
		house.setHousenumber(hn);
		house.setStreet(getStreetname(el));
		house.setPlace(el.getTag(TK_ADDR_PLACE));
		String zipStr = el.getTag(TKM_POSTAL_CODE);
		ZipCodeInfo zip = getZipInfos(zipStr);
		house.setZipCode(zip);
		return house;
	}
	
	private CityInfo getCityInfos(String city, String region, String country) {
		CityInfo ci = new CityInfo(city, region, country);
		CityInfo ciOld = cityInfos.get(ci);
		if (ciOld != null)
			return ciOld;
		cityInfos.put(ci, ci);
		return ci;
	}

	private ZipCodeInfo getZipInfos(String zipStr) {
		ZipCodeInfo zip = new ZipCodeInfo(zipStr);
		ZipCodeInfo zipOld = zipInfos.get(zip);
		if (zipOld != null)
			return zipOld;
		zipInfos.put(zip, zip);
		return zip;
	}

	private HousenumberElem handleElement(Element el) {
		String sign = getHousenumber(el);
		if (sign == null)
			return null;
		
		HousenumberElem he = parseElement(el, sign);
		if (he == null)
			return null;
		houseElems.add(he);
		return he;
	}
	/**
	 * Adds a node for house number processing.
	 * @param n an OSM node
	 */
	public void addNode(Node n) {
		if (!numbersEnabled) {
			return;
		}
		if("false".equals(n.getTag(TKM_NUMBERS)))
			return;
		
		if ("true".equals(n.getTag(POIGeneratorHook.TKM_AREA2POI))) {
			// ignore POI created for buildings
			return; 		
		}
		HousenumberElem houseElem = handleElement(n);
		if (houseElem == null)
			return;
		
		if (n.getTag(HousenumberHooks.TKM_PART_OF_INTERPOLATION) != null)
			interpolationNodes.put(n.getId(),houseElems.size()-1);
	}
	
	/**
	 * Adds a way for house number processing.
	 * @param w a way
	 */
	public void addWay(Way w) {
		if (!numbersEnabled) {
			return;
		}
		if("false".equals(w.getTag(TKM_NUMBERS)))
			return;
		
		String ai = w.getTag(TK_ADDR_INTERPOLATION);
		if (ai != null) {
			// the way has the addr:interpolation=* tag, parse info
			// created by the HousenumberHook
			List<HousenumberElem> nodes = new ArrayList<>();
			String nodeIds = w.getTag(HousenumberHooks.TKM_NODE_IDS);
			if (nodeIds == null) {
				// way was rejected by hook
			} else {
				String[] ids = nodeIds.split(",");
				for (String idString : ids) {
					Long id = Long.decode(idString);
					Integer elemPos = interpolationNodes.get(id);
					if (elemPos != null) {
						HousenumberElem node = houseElems.get(elemPos);
						if (node != null) {
							assert node.getElement().getId() == id;
							nodes.add(node);
						}
					}
				}
				interpretInterpolationWay(w, nodes);
			}
			return;
		}
		
		if (w.hasIdenticalEndPoints()) {
			// we are only interested in polygons now
			handleElement(w);
		}

	}
	
	/**
	 * Use the information provided by the addr:interpolation tag
	 * to generate additional house number elements. This increases
	 * the likelihood that a road segment is associated with the right 
	 * number ranges. 
	 * @param w the way
	 * @param nodes2 
	 * @param nodes list of nodes
	 */
	private void interpretInterpolationWay(Way w, List<HousenumberElem> nodes) {
		int numNodes = nodes.size();
		String addrInterpolationMethod = w.getTag(TK_ADDR_INTERPOLATION);
		int step = 0;
		switch (addrInterpolationMethod) {
		case "all":
		case "1":
			step = 1;
			break;
		case "even":
		case "odd":
		case "2":
			step = 2;
			break;
		default:
			break;
		}
		if (step == 0)
			return; // should not happen here
		int pos = 0;
		List<HousenumberIvl> hivls = new ArrayList<>();
		String streetName = null;
		for (int i = 0; i+1 < numNodes; i++) {
			// the way have other points, find the sequence including the pair of nodes    
			HousenumberElem he1 = nodes.get(i);
			HousenumberElem he2 = nodes.get(i+1);
			int pos1 = -1;
			for (int k = pos; k < w.getPoints().size(); k++) {
				if (w.getPoints().get(k) == he1.getLocation()) {
					pos1 = k;
					break;
				}
			}
			if (pos1 < 0) {
				log.error("addr:interpolation node not found in way",w);
				return;
			}
			int pos2 = -1;
			for (int k = pos1+1; k < w.getPoints().size(); k++) {
				if (w.getPoints().get(k) == he2.getLocation()) {
					pos2 = k;
					break;
				}
			}
			if (pos2 < 0) {
				log.error("addr:interpolation node not found in way",w);
				return;
			}
			pos = pos2;
			String street = he1.getStreet();
			if (street != null && street.equals(he2.getStreet())) {
				if (streetName == null)
					streetName = street;
				else if (!streetName.equals(street)) {
					log.warn(w.toBrowseURL(),"addr:interpolation=even is used with different street names",streetName,street);
					return;
				}
				
				int start = he1.getHousenumber();
				int end = he2.getHousenumber();
				HousenumberIvl hivl = new HousenumberIvl(street, w, (Node)he1.element, (Node)he2.element);
				hivl.setStart(start);
				hivl.setEnd(end);
				hivl.setStep(step);
				hivl.calcSteps();
				hivl.setPoints(w.getPoints().subList(pos1, pos2+1));
				
				hivls.add(hivl);
				if ("even".equals(addrInterpolationMethod) && (start % 2 != 0 || end % 2 != 0)) {
					log.warn(w.toBrowseURL(),"addr:interpolation=even is used with odd housenumber(s)",start,end);
					return;
				}
				if ("odd".equals(addrInterpolationMethod) && (start % 2 == 0 || end % 2 == 0)) {
					log.warn(w.toBrowseURL(),"addr:interpolation=odd is used with even housenumber(s)",start,end);
					return;
				}
				
				if (start == end && he1.getSign().equals(he2.getSign()) && pos1 == 0
						&& pos2 + 1 == w.getPoints().size()) {
					// handle special case from CanVec imports
					hivl.setEqualEnds();
					log.warn(w.toBrowseURL(),
							"addr:interpolation way connects two points with equal numbers, numbers are ignored");
				}
			}
		}
		for (HousenumberIvl  hivl : hivls)
			interpolationWays.add(streetName, hivl);
	}

	private MapRoad firstRoadSameOSMWay = null;
	/**
	 * Adds a road to be processed by the house number generator.
	 * @param osmRoad the OSM way the defines the road 
	 * @param road a road
	 */
	public void addRoad(Way osmRoad, MapRoad road) {
		allRoads.add(road);
		if (numbersEnabled) {
			if("false".equals(osmRoad.getTag(TKM_NUMBERS))) 
				road.setSkipHousenumberProcessing(true);
			
			/*
			 * If the style adds the same OSM way as two or more routable ways, we use
			 * only the first. This ensures that we don't try to assign numbers from bad
			 * matches to these copies.
			 */
			if(!road.isSkipHousenumberProcessing()) {
				if (firstRoadSameOSMWay != null 
						&& firstRoadSameOSMWay.getRoadDef().getId() == road.getRoadDef().getId()
						&& firstRoadSameOSMWay.getPoints().equals(road.getPoints())) {
					road.setSkipHousenumberProcessing(true);
					return;
				}
				road.getPoints().get(0).setNumberNode(true);
				road.getPoints().get(road.getPoints().size() - 1).setNumberNode(true);
				firstRoadSameOSMWay = road;
				String name = road.getStreet(); 
				if (name != null && log.isDebugEnabled()) {
					log.debug("Housenumber - Streetname:", name, "Way:",osmRoad.getId(),osmRoad.toTagString());
				}
			}
		} 
	}
	
	/**
	 * Evaluate type=associatedStreet relations.
	 */
	public void addRelation(Relation r) {
		String relType = r.getTag("type");
		// the wiki says that we should also evaluate type=street
		if ("associatedStreet".equals(relType) || "street".equals(relType)) {
			List<Element> houses= new ArrayList<>();
			List<Element> streets = new ArrayList<>();
			for (Map.Entry<String, Element> member : r.getElements()) {
				if (member.getValue() instanceof Node) {
					Node node = (Node) member.getValue();
					houses.add(node);
				} else if (member.getValue() instanceof Way) {
					Way w = (Way) member.getValue();
					String role = member.getKey();
					switch (role) {
					case "house":
					case "addr:houselink":
					case "address":
						houses.add(w);
						break;
					case "street":
						streets.add(w);
						break;
					case "":
						if (w.getTag("highway") != null) {
							streets.add(w);
							continue;
						}
						String buildingTag = w.getTag("building");
						if (buildingTag != null)
							houses.add(w);
						else 
							log.warn("Relation",r.toBrowseURL(),": role of member",w.toBrowseURL(),"unclear");
						break;
					default:
						if ("associatedStreet".equals(relType)) 
							log.warn("Relation",r.toBrowseURL(),": don't know how to handle member with role",role);
						break;
					}
				}
			}
			String streetName = r.getTag("name");
			String streetNameFromRoads = null;
			List<Element> unnamedStreetElems = new ArrayList<>();
			boolean nameFromStreetsIsUnclear = false;
			if (!streets.isEmpty()) {
				for (Element street : streets) {
					String roadName = street.getTag(TKM_STREET);
					if (roadName == null) 
						roadName = street.getTag("name");
					if (roadName == null) {
						unnamedStreetElems.add(street);
						continue;
					}
					if (streetNameFromRoads == null)
						streetNameFromRoads = roadName;
					else if (!streetNameFromRoads.equals(roadName)) {
						nameFromStreetsIsUnclear = true;
					}
				}
			}
			if (streetName == null) {
				if (!nameFromStreetsIsUnclear) {
					streetName = streetNameFromRoads;
				} else {
					log.warn("Relation",r.toBrowseURL(),": ignored, street name is not clear.");
					return;
				}

			} else {
				if (streetNameFromRoads != null) {
					if (!nameFromStreetsIsUnclear && !streetName.equals(streetNameFromRoads)) {
						if (!unnamedStreetElems.isEmpty()) {
							log.warn("Relation",r.toBrowseURL(),": ignored, street name is not clear.");
							return;
						}
						log.warn("Relation",r.toBrowseURL(),": street name is not clear, using the name from the way, not that of the relation.");
						streetName = streetNameFromRoads;
					} 
					else if (nameFromStreetsIsUnclear) {
						log.warn("Relation",r.toBrowseURL(),": street name is not clear, using the name from the relation.");
					}
				} 
			}
			int countModHouses = 0;
			if (streetName != null && !streetName.isEmpty()) {
				for (Element house : houses) {
					if (addStreetTagFromRel(r, house, streetName))
						countModHouses++;
				}
				for (Element street : unnamedStreetElems) {
					street.addTag(TKM_STREET, streetName);
					street.addTag("name", streetName);
				}
			}
			if (log.isInfoEnabled()) {
				if (countModHouses > 0 || !unnamedStreetElems.isEmpty()) {
					if (countModHouses > 0)
						log.info("Relation",r.toBrowseURL(),": added tag mkgmap:street=",streetName,"to",countModHouses,"of",houses.size(),"house members" );
					if (!unnamedStreetElems.isEmpty())
						log.info("Relation",r.toBrowseURL(),": added tag mkgmap:street=",streetName,"to",unnamedStreetElems.size(),"of",streets.size(),"street members" );
				}
				else 
					log.info("Relation",r.toBrowseURL(),": ignored, no house or street member was changed");
			}

		}
	}
	
	/**
	 * Add the tag mkgmap:street=streetName to the element of the 
	 * relation if it does not already have a street name tag.
	 */
	private static boolean addStreetTagFromRel(Relation r, Element house, String streetName) {
		String addrStreet = getStreetname(house);
		if (addrStreet == null) {
			house.addTag(TKM_STREET, streetName);
			if (log.isDebugEnabled())
				log.debug("Relation",r.toBrowseURL(),": adding tag mkgmap:street=" + streetName, "to house",house.toBrowseURL());
			return true;
		}
		if (!addrStreet.equals(streetName)) {
			if (house.getTag(TKM_STREET) != null) {
				log.warn("Relation",r.toBrowseURL(),": street name from relation doesn't match existing mkgmap:street tag for house",house.toBrowseURL(),"the house seems to be member of another type=associatedStreet relation");
				house.deleteTag(TKM_STREET);
			} else { 
				log.warn("Relation",r.toBrowseURL(),": street name from relation doesn't match existing name for house",house.toBrowseURL());
			}
		}
		return false;
	}
	
	
	/**
	 * Calculate number nodes for the collected roads  
	 * @param adder 
	 */
	public void generate(LineAdder adder) {
		if (numbersEnabled) {
			MultiHashMap<MapRoad,HousenumberMatch> initialHousesForRoads = findClosestRoadsToHouse();
			identifyServiceRoads();
			
			handleInterpolationWays(initialHousesForRoads);
			
			List<HousenumberRoad> hnrList = createHousenumberRoads(initialHousesForRoads);
			initialHousesForRoads = null;
			log.info("found",hnrList.size(),"road candidates for address search");
			
			useAddrPlaceTag();
			Map<MapRoad, HousenumberRoad> road2HousenumberRoadMap = new HashMap<>();
			for (HousenumberRoad hnr : hnrList) {
				road2HousenumberRoadMap.put(hnr.getRoad(), hnr);
			}
			Int2ObjectOpenHashMap<HashSet<MapRoad>> nodeId2RoadLists = new Int2ObjectOpenHashMap<>();
			for (MapRoad road : allRoads) {
				for (Coord co : road.getPoints()) {
					if (co.getId() == 0)
						continue;
					HashSet<MapRoad> connectedRoads = nodeId2RoadLists.get(co.getId());
					if (connectedRoads == null) {
						connectedRoads = new HashSet<>();
						nodeId2RoadLists.put(co.getId(), connectedRoads);
					}
					connectedRoads.add(road);
				}
			}
			List<HousenumberRoad> addedRoads = new ArrayList<>();
			Iterator<HousenumberRoad> iter = hnrList.iterator();
			while (iter.hasNext()) {
				HousenumberRoad hnr = iter.next();
				
				List<HousenumberMatch> lostHouses = hnr.checkStreetName(nodeId2RoadLists);
				for (HousenumberMatch house : lostHouses) {
					MapRoad r = house.getRoad();
					if (r != null) {
						HousenumberRoad hnr2 = road2HousenumberRoadMap.get(r);
						if (hnr2 == null) {
							CityInfo ci = getCityInfos(r.getCity(), r.getRegion(), r.getCountry());
							hnr2 = new HousenumberRoad(r, ci, Arrays.asList(house));
							if (r.getZip() != null)
								hnr2.setZipCodeInfo(getZipInfos(r.getZip()));
							road2HousenumberRoadMap.put(r,hnr2);
							addedRoads.add(hnr2);
						} else {
							hnr2.addHouse(house);
						}
					}
				}
				if (hnr.getName() == null) {
					iter.remove();
					for (HousenumberMatch house : hnr.getHouses()) {
						log.warn("found no plausible road name for address",house.toBrowseURL(),", closest road id:",house.getRoad());
					}
				}
			}
			hnrList.addAll(addedRoads);
			// TODO: interpolate addr:interpolation houses
			removeDupsGroupedByCityAndName();
			
			// group by street name and city
			TreeMap<String, TreeMap<CityInfo, List<HousenumberRoad>>> streetnameCityRoadMap = new TreeMap<>();
			for (HousenumberRoad hnr : hnrList) {
				TreeMap<CityInfo, List<HousenumberRoad>> cluster = streetnameCityRoadMap.computeIfAbsent(hnr.getName(),
						k -> new TreeMap<>());
				cluster.computeIfAbsent(hnr.getRoadCityInfo(), k -> new ArrayList<>()).add(hnr);
			}
			
			for (Entry<String, TreeMap<CityInfo, List<HousenumberRoad>>> streetNameEntry : streetnameCityRoadMap.entrySet()) {
				String streetName = streetNameEntry.getKey();
				
				for (Entry<CityInfo, List<HousenumberRoad>> clusterEntry : streetNameEntry.getValue().entrySet()) {
					useInterpolationInfo(streetName, clusterEntry.getValue(), road2HousenumberRoadMap);
				}
				
				for (Entry<CityInfo, List<HousenumberRoad>> clusterEntry : streetNameEntry.getValue().entrySet()) {
					List<HousenumberRoad> roadsInCluster = clusterEntry.getValue();
					if (log.isDebugEnabled()) {
						log.debug("processing road(s) with name",streetName,"in",clusterEntry.getKey() );
					}
					for (HousenumberRoad hnr : roadsInCluster) {
						hnr.buildIntervals();
					}
					boolean optimized = false;
					for (int loop = 0; loop < 10; loop++) {
						for (HousenumberRoad hnr : roadsInCluster) {
							hnr.checkIntervals();
						}
						checkWrongRoadAssignmments(roadsInCluster);
						boolean changed = hasChanges(roadsInCluster);
						if (!optimized && !changed) {
							for (HousenumberRoad hnr : roadsInCluster) {
								hnr.improveSearchResults();
							}
							changed = hasChanges(roadsInCluster);
							optimized = true;
						}
						if (!changed)
							break;
					}
					for (HousenumberRoad hnr : roadsInCluster) {
						hnr.setNumbers();
					}
				}
			}
		}
			
		if (log.isInfoEnabled()) {
			for (HousenumberElem house : houseElems) {
				if (house.getRoad() == null) {
					if (house.getStreet() != null)
						log.info("found no plausible road for house number element",house.toBrowseURL(),house.getStreet(),house.getSign());
					else 
						log.info("found no plausible road for house number element",house.toBrowseURL());
				}
			}
		}
		for (MapRoad r : allRoads) {
			if (log.isDebugEnabled()) {
				List<Numbers> finalNumbers = r.getRoadDef().getNumbersList();
				if (finalNumbers != null) {
					log.info("id:"+r.getRoadDef().getId(),", final numbers,",r,"in",r.getCity());
					for (Numbers cn : finalNumbers) {
						if (cn.isEmpty())
							continue;
						log.info("id:"+r.getRoadDef().getId(),", Left: ",cn.getLeftNumberStyle(),cn.getIndex(),"Start:",cn.getLeftStart(),"End:",cn.getLeftEnd());
						log.info("id:"+r.getRoadDef().getId(),", Right:",cn.getRightNumberStyle(),cn.getIndex(),"Start:",cn.getRightStart(),"End:",cn.getRightEnd());
					}
				}
			}
			adder.add(r);
		}
	}
	
	private List<HousenumberRoad> createHousenumberRoads(
			MultiHashMap<MapRoad, HousenumberMatch> initialHousesForRoads) {
		List<HousenumberRoad> hnrList = new ArrayList<>();
		for (MapRoad road : allRoads) {
			if (road.isSkipHousenumberProcessing())
				continue;
			List<HousenumberMatch> houses = initialHousesForRoads.get(road);
			if (houses == null || houses.isEmpty())
				continue;
			CityInfo ci = getCityInfos(road.getCity(), road.getRegion(), road.getCountry());
			HousenumberRoad hnr = new HousenumberRoad(road, ci, houses);
			if (road.getZip() != null)
				hnr.setZipCodeInfo(getZipInfos(road.getZip()));
				
			hnrList.add(hnr);
		}
		return hnrList;
	}

	private MultiHashMap<MapRoad, HousenumberMatch> findClosestRoadsToHouse() {
		// build road index
		long t1 = System.currentTimeMillis();
		RoadSegmentIndex roadSegmentIndex = new RoadSegmentIndex(allRoads, MAX_DISTANCE_TO_ROAD);
		long t2 = System.currentTimeMillis();
		log.debug("creation of road index took",t2-t1,"ms");
		
		long t3 = System.currentTimeMillis();
		MultiHashMap<MapRoad,HousenumberMatch> initialHousesForRoads = new MultiHashMap<>();
		for (int i = 0; i < houseElems.size(); i++) {
			HousenumberElem house = houseElems.get(i);
			HousenumberMatch bestMatch = roadSegmentIndex.createHousenumberMatch(house);
			houseElems.set(i, bestMatch);
			if (bestMatch.getRoad() == null) {
				bestMatch.setIgnored(true); // XXX maybe create a pseudo road with zero length?
//				log.warn("found no plausible road for house number element",house.getElement().toBrowseURL());
				continue;
			}
			initialHousesForRoads.add(bestMatch.getRoad(), bestMatch);
		}
		long t4 = System.currentTimeMillis();
		log.debug("identification of closest road for each house took",t4-t3,"ms");
		
		return initialHousesForRoads;
	}

	private void useAddrPlaceTag() {
		HashMap<CityInfo,MultiHashMap<String,HousenumberMatch>> cityPlaceHouseMap = new LinkedHashMap<>();
		for (int i = 0; i < houseElems.size(); i++) {
			HousenumberElem house = houseElems.get(i);
			if (house.getRoad() == null || house.getPlace() == null)
				continue;
			if (house instanceof HousenumberMatch) {
				HousenumberMatch hm = (HousenumberMatch) house;
				if (hm.getHousenumberRoad() != null) {
					cityPlaceHouseMap.computeIfAbsent(house.getCityInfo(), k -> new MultiHashMap<>())
							.add(house.getPlace(), (HousenumberMatch) house);
				}
			}
		}
		log.info("analysing",cityPlaceHouseMap.size(),"cities with addr:place=* houses" );
		for (Entry<CityInfo, MultiHashMap<String, HousenumberMatch>> topEntry : cityPlaceHouseMap.entrySet()) {
			CityInfo cityInfo = topEntry.getKey();
			List<String> placeNames = new ArrayList<>(topEntry.getValue().keySet());
			placeNames.sort(null);
			for (String placeName : placeNames) {
				List<HousenumberMatch> placeHouses = topEntry.getValue().get(placeName);
				HashSet<HousenumberRoad> roads = new LinkedHashSet<>();
				Int2IntOpenHashMap usedNumbers = new Int2IntOpenHashMap();
				HashMap<String,Integer> usedSigns = new HashMap<>();
				int dupSigns = 0;
				int dupNumbers = 0;
				int housesWithStreet = 0;
				int housesWithMatchingStreet = 0;
				int roadsWithNames = 0;
				int unnamedCloseRoads = 0;
				
				for (HousenumberMatch house : placeHouses) {
					if (house.getStreet() != null ) {
						++housesWithStreet;
						if (house.getStreet().equalsIgnoreCase(house.getRoad().getStreet())) {
							++housesWithMatchingStreet;
						}
					} else {
						if (house.getRoad().getStreet() == null)
							++unnamedCloseRoads;
					}
					boolean added = roads.add(house.getHousenumberRoad());
					if (added && house.getRoad().getStreet() != null)
						++roadsWithNames;
					int oldCount = usedNumbers.put(house.getHousenumber(),1);
					if (oldCount != 0) {
						usedNumbers.put(house.getHousenumber(), oldCount + 1);
						++dupNumbers;
					}
					Integer oldSignCount = usedSigns.put(house.getSign(), 1);
					if (oldSignCount != null) {
						usedSigns.put(house.getSign(), oldSignCount + 1);
						++dupSigns;
					}
				}
				
				if (log.isDebugEnabled()) {
					log.debug("place",placeName,"in city",cityInfo, ":", "houses:", placeHouses.size(),
							",duplicate numbers/signs:", dupNumbers+"/"+dupSigns,
							",roads (named/unnamed):", roads.size(),"("+roadsWithNames+"/"+(roads.size()- roadsWithNames)+")",
							",houses without addr:street:", placeHouses.size() - housesWithStreet,
							",street = name of closest road:", housesWithMatchingStreet,
							",houses without addr:street near named road:",	unnamedCloseRoads);
				}
				if ((float) dupSigns / placeHouses.size() < 0.25 ) {
					if (log.isDebugEnabled())
						log.debug("will not use gaps in intervals for roads in",placeName );
					for (HousenumberRoad hnr : roads) {
						hnr.setRemoveGaps(true);
					}
				}
				if (placeHouses.size() > housesWithStreet) { // XXX: threshold value?
					LongArrayList ids = new LongArrayList();
					for (HousenumberRoad hnr : roads) {
						ids.add(hnr.getRoad().getRoadDef().getId());
						hnr.addPlaceName(placeName);
					}
					if (log.isDebugEnabled())
						log.debug("detected",placeName,"as potential address name for roads",ids);
				} else {
					if (log.isDebugEnabled())
						log.debug("will ignore addr:place for address search in",placeName,"in city",cityInfo);
				}
			}
		}
	}

	/**
	 * Update the house number information in the interpolation intervals and 
	 * use them to correct wrong road assignments.
	 * @param initialHousesForRoads map that is updated when wrong road assignments were found
	 */
	private void handleInterpolationWays(MultiHashMap<MapRoad, HousenumberMatch> initialHousesForRoads) {
		for (Entry<String, List<HousenumberIvl>> entry : interpolationWays.entrySet()) {
			List<HousenumberIvl> infos = entry.getValue();
			for (HousenumberIvl info : infos) {
				if (info.isBad()) {
					continue;
				}
				boolean isOK = info.setNodeRefs(interpolationNodes, houseElems);
				if (!isOK)
					continue;
				HousenumberMatch[] houses = info.getHouseNodes();
				MapRoad[] uncheckedRoads = new MapRoad[houses.length];
				for (int i = 0 ; i < houses.length; i++)
					uncheckedRoads[i] = houses[i].getRoad();
				info.checkRoads();
				// check if houses are assigned to different roads now
				houses = info.getHouseNodes();
				for (int i = 0 ; i < houses.length; i++) {
					if (houses[i].getRoad() != uncheckedRoads[i]) {
						initialHousesForRoads.removeMapping(uncheckedRoads[i], houses[i]);
						if (!houses[i].isIgnored()) {
							initialHousesForRoads.add(houses[i].getRoad(), houses[i]);
						}
					}
				}
			}
		}
	}

	private void removeDupsGroupedByCityAndName() {
		HashMap<CityInfo,MultiHashMap<String,HousenumberMatch>> cityNameHouseMap = new LinkedHashMap<>();
		for (HousenumberElem house : houseElems) {
			if (house.getRoad() == null)
				continue;
			if (house instanceof HousenumberMatch) {
				HousenumberMatch hm = (HousenumberMatch) house;
				if (hm.isIgnored())
					continue;
				HousenumberRoad hnr = hm.getHousenumberRoad();
				if (hnr == null || hnr.getName() == null)
					continue;
				cityNameHouseMap.computeIfAbsent(hm.getCityInfo(), k -> new MultiHashMap<>()).add(hnr.getName(), hm);
			}
		}

		for (Entry<CityInfo, MultiHashMap<String, HousenumberMatch>> topEntry : cityNameHouseMap.entrySet()) {
			for (Entry<String, List<HousenumberMatch>> entry : topEntry.getValue().entrySet()) {
				markSimpleDuplicates(entry.getKey(), entry.getValue());
			}
		}
	}	
	
	/**
	 * process option --x-name-service-roads=n
	 * The program identifies unnamed roads which are only connected to one
	 * road with a name or to multiple roads with the same name. The process is
	 * repeated n times. If n > 1 the program will also use unnamed roads which
	 * are connected to unnamed roads if those are connected to named roads.
	 * Higher values for n mean deeper search, but reasonable values are
	 * probably between 1 and 5.
	 * 
	 * These roads are then used for house number processing like the named
	 * ones. If house numbers are assigned to these roads, they are named so
	 * that address search will find them.
	 */
	private void identifyServiceRoads() {
		Int2ObjectOpenHashMap<String> roadNamesByNodeIds = new Int2ObjectOpenHashMap<>();
		HashMap<MapRoad, List<Coord>> coordNodesUnnamedRoads = new HashMap<>();
		HashSet<Integer> unclearNodeIds = new HashSet<>();

		long t1 = System.currentTimeMillis();

		List<MapRoad> unnamedRoads = new ArrayList<>();
		for (MapRoad road : allRoads) {
			if (road.isSkipHousenumberProcessing())
				continue;
			
			if (road.getStreet() == null) {
				// if a road has a label but getStreet() returns null,
				// the road probably has a ref. We assume these are not service roads. 
				if (road.getName() == null) {
					unnamedRoads.add(road);
					List<Coord> nodes = new ArrayList<>();
					for (Coord co : road.getPoints()) {
						if (co.getId() != 0)
							nodes.add(co);
					}
					coordNodesUnnamedRoads.put(road, nodes);
				}
			} else {
				identifyNodes(road.getPoints(), road.getStreet(), roadNamesByNodeIds, unclearNodeIds);
			}
		}
		int numUnnamedRoads = unnamedRoads.size();
		long t2 = System.currentTimeMillis();
		if (log.isDebugEnabled())
			log.debug("identifyServiceRoad step 1 took",(t2-t1),"ms, found",roadNamesByNodeIds.size(),"nodes to check and",numUnnamedRoads,"unnamed roads" );
		long t3 = System.currentTimeMillis();
		int named = 0;
		for (int pass = 1; pass <= nameSearchDepth; pass ++) {
			int unnamed = 0;
			List<MapRoad> namedRoads = new ArrayList<>();
			for (int j = 0; j < unnamedRoads.size(); j++) {
				MapRoad road = unnamedRoads.get(j);
				if (road == null)
					continue;
				unnamed++;
				List<Coord> coordNodes = coordNodesUnnamedRoads.get(road); 
				String name = null;
				for (Coord co : coordNodes) {
					if (unclearNodeIds.contains(co.getId())) {
						name = null;
						unnamedRoads.set(j, null); // don't process again
						break;
					}
					String possibleName = roadNamesByNodeIds.get(co.getId());
					if (possibleName == null)
						continue;
					if (name == null)
						name = possibleName;
					else if (!name.equals(possibleName)) {
						name = null;
						unnamedRoads.set(j, null); // don't process again
						break;
					}
				}
				if (name != null) {
					named++;
					road.setStreet(name);
					namedRoads.add(road);
					unnamedRoads.set(j, null); // don't process again
				}
			}
			for (MapRoad road : namedRoads) {
				road.setNamedByHousenumberProcessing(true);
				road.setNamedByIdentifyServiceRoad(true);
				String name = road.getStreet();
				if (log.isDebugEnabled())
					log.debug("pass",pass,"using unnamed road for housenumber processing,id=",road.getRoadDef().getId(),":",name);
				List<Coord> coordNodes = coordNodesUnnamedRoads.get(road); 
				identifyNodes(coordNodes, name, roadNamesByNodeIds, unclearNodeIds);
			}

			if (namedRoads.isEmpty())
				break;
			if (log.isDebugEnabled())
				log.debug("pass",pass,unnamed,named);
		}
		long t4 = System.currentTimeMillis();
		if (log.isDebugEnabled()) {
			log.debug("indentifyServiceRoad step 2 took",(t4-t3),"ms, found a name for",named,"of",numUnnamedRoads,"roads" );
		}
	}

	private void identifyNodes(List<Coord> roadPoints,
			String streetName, Int2ObjectOpenHashMap<String> roadNamesByNodeIds, HashSet<Integer> unclearNodes) {
		for (Coord co : roadPoints) {
			if (co.getId() != 0) {
				String prevName = roadNamesByNodeIds.put(co.getId(), streetName);
				if (prevName != null && !prevName.equals(streetName)) {
					unclearNodes.add(co.getId());
				}
			}
		}			
	}


	/**
	 * Find house number nodes which are parts of addr:interpolation ways.
	 *  Check each addr:interpolation way for plausibility. 
	 *  If the check is OK, interpolate the numbers, if not, ignore 
	 *  also the numbers connected to the implausible way.
	 *  
	 *  XXX: Known problem: Doesn't work well when the road was
	 *  clipped at the tile boundary.
	 * @param streetName
	 * @param roadsInCluster
	 * @param road2HousenumberRoadMap 
	 * @param interpolationInfos 
	 */
	private void useInterpolationInfo(String streetName,
			List<HousenumberRoad> roadsInCluster, Map<MapRoad, HousenumberRoad> road2HousenumberRoadMap) {
		List<HousenumberIvl> interpolationInfos = interpolationWays.get(streetName);
		if (interpolationInfos.isEmpty())
			return;
		List<HousenumberMatch> housesWithIvlInfo = new ArrayList<>();
		for (HousenumberRoad hnr : roadsInCluster) {
			for (HousenumberMatch house : hnr.getHouses()) {
				if (house.getIntervalInfoRefs() > 0)
					housesWithIvlInfo.add(house);
			}
		}
		if (housesWithIvlInfo.isEmpty())
			return;
		
		HashMap<String, HousenumberIvl> simpleDupCheckSet = new HashMap<>();
		HashSet<HousenumberIvl> badIvls = new HashSet<>();
		Long2ObjectOpenHashMap<HousenumberIvl> id2IvlMap = new Long2ObjectOpenHashMap<>();
		Int2ObjectOpenHashMap<HousenumberMatch> interpolatedNumbers = new Int2ObjectOpenHashMap<>();
		Int2ObjectOpenHashMap<HousenumberMatch> existingNumbers = new Int2ObjectOpenHashMap<>();
		HashMap<HousenumberIvl, List<HousenumberMatch>> housesToAdd = new LinkedHashMap<>();
		
		for (HousenumberRoad hnr : roadsInCluster) {
			for (HousenumberMatch house : hnr.getHouses())
				existingNumbers.put(house.getHousenumber(), house);
		}
		int inCluster = 0;
		boolean allOK = true;
		// for loop may change the list 
		for (int i = 0; i < interpolationInfos.size(); i++) {
			HousenumberIvl hivl = interpolationInfos.get(i);
			if (!hivl.inCluster(housesWithIvlInfo) || hivl.ignoreForInterpolation())
				continue;
			++inCluster;
			String hivlDesc = hivl.getDesc();
			HousenumberIvl hivlTest = simpleDupCheckSet.get(hivlDesc);
			if (hivlTest != null) {
				// happens often in Canada (CanVec imports): two or more addr:interpolation ways with similar meaning
				// sometimes at completely different road parts, sometimes at exactly the same
				log.warn("found additional addr:interpolation way with same meaning, is ignored:",streetName, hivl, hivlTest);
				badIvls.add(hivl);
				allOK = false;
				continue;
			}
			simpleDupCheckSet.put(hivlDesc, hivl);

			id2IvlMap.put(hivl.getId(), hivl);
			List<HousenumberMatch> interpolatedHouses = hivl.getInterpolatedHouses();
			if (!interpolatedHouses.isEmpty()) {
				if (interpolatedHouses.get(0).getRoad() == null) {
					// the interpolated houses are not all along one road
					findRoadForInterpolatedHouses(streetName, interpolatedHouses, roadsInCluster);
				}
				
				int dupCount = 0;
				for (HousenumberMatch house : interpolatedHouses) {
					if (house.getRoad() == null || house.getDistance() > HousenumberIvl.MAX_INTERPOLATION_DISTANCE_TO_ROAD)
						continue;
					boolean ignoreGenOnly = false;
					HousenumberMatch old = interpolatedNumbers.get(house.getHousenumber());
					if (old == null) {
						ignoreGenOnly = true;
						old = existingNumbers.get(house.getHousenumber());
					}
					if (old != null) {
						// try to build new intervals using existing node
						HousenumberIvl[] splitIvls = hivl.trySplitAt(old);
						if (splitIvls != null) {
							log.info("adding address",streetName,old,old.toBrowseURL(),"to addr:interpolation way, replacing", hivl,"by",Arrays.deepToString(splitIvls));
							interpolationInfos.add(splitIvls[0]);
							interpolationInfos.add(splitIvls[1]);
							hivl.setIgnoreForInterpolation(true);
							break;
						}
						// forget both or only one ? Which one?
						house.setIgnored(true);
						double distToOld = old.getLocation().distance(house.getLocation()); 
						if (distToOld > MAX_DISTANCE_SAME_NUM) {
							if (old.isInterpolated())
								log.info("conflict caused by addr:interpolation way", streetName, hivl, "and interpolated address", old, "at", old.getLocation());
							else 
								log.info("conflict caused by addr:interpolation way", streetName, hivl, "and address element", old, "at", old.getLocation());
							dupCount++;
							if (!ignoreGenOnly) {
								old.setIgnored(true);
								long ivlId = old.getElement().getOriginalId();
								HousenumberIvl bad = id2IvlMap.get(ivlId);
								if (bad != null)
									badIvls.add(bad);
							}
						}
					}
				}
				if (hivl.ignoreForInterpolation())
					continue;
				if (dupCount > 0) {
					log.warn("addr:interpolation way",streetName,hivl,"is ignored, it produces",dupCount,"duplicate number(s) too far from existing nodes");
					badIvls.add(hivl);
				} else {
					housesToAdd.put(hivl, interpolatedHouses);
					for (HousenumberMatch hnm : interpolatedHouses)
						interpolatedNumbers.put(hnm.getHousenumber(), hnm);
				}
			}
		}
		if (inCluster == 0)
			return;
		for (HousenumberIvl badIvl: badIvls) {
			allOK = false;
			badIvl.ignoreNodes();
			housesToAdd.remove(badIvl);
		}
		Iterator<Entry<HousenumberIvl, List<HousenumberMatch>>> iter = housesToAdd.entrySet().iterator();
		while (iter.hasNext()) {
			Entry<HousenumberIvl, List<HousenumberMatch>> entry = iter.next();
			if (log.isInfoEnabled())
				log.info("using generated house numbers from addr:interpolation way",entry.getKey());
			for (HousenumberMatch house : entry.getValue()) {
				if (house.getRoad() != null && !house.isIgnored()) {
					HousenumberRoad hnr = road2HousenumberRoadMap.get(house.getRoad());
					if (hnr == null) {
						log.error("internal error: found no housenumber road for interpolated house number", house.getHousenumber(),house.toBrowseURL());
						continue;
					}
					hnr.addHouse(house);
				}
			}
		}
		if (log.isDebugEnabled()) {
			if (allOK)
				log.debug("found no problems with interpolated numbers from addr:interpolations ways for roads with name",streetName);
			else 
				log.debug("found problems with interpolated numbers from addr:interpolations ways for roads with name",streetName);
		}
	}
	
	private static void findRoadForInterpolatedHouses(String streetName,
			List<HousenumberMatch> houses,
			List<HousenumberRoad> roadsInCluster) {
		if (houses.isEmpty())
			return;
		houses.sort(new HousenumberMatchByNumComparator());
		
		HousenumberMatch prev = null;
		for (HousenumberMatch house : houses) {
			if (house.isIgnored())
				continue;
			house.setDistance(Double.POSITIVE_INFINITY); // make sure that we don't use an old match
			house.setRoad(null);
			List<HousenumberMatch> matches = new ArrayList<>();
			for (HousenumberRoad hnr : roadsInCluster) {
				MapRoad r = hnr.getRoad();
				// make sure that we use the street info if available
				if (house.getPlace() != null && house.getStreet() != null && r.getStreet() != null
						&& house.getStreet().equals(r.getStreet())) {
					continue;
				}
				HousenumberMatch test = new HousenumberMatch(house);
				findClosestRoadSegment(test, r);
				if (test.getRoad() != null && test.getGroup() != null || test.getDistance() < MAX_DISTANCE_TO_ROAD) {
					matches.add(test);
				}
			}
			if (matches.isEmpty()) {
				house.setIgnored(true);
				continue;
			}			
			
			HousenumberMatch closest = matches.get(0); 
			HousenumberMatch best = closest;
			
			if (matches.size() > 1) {
				// multiple roads, we assume that the closest is the best
				// but we may have to check the alternatives as well
				
				matches.sort(new HousenumberGenerator.HousenumberMatchByDistComparator());
				closest  = matches.get(0);
				best = checkAngle(closest, matches);	
			}
			house.setDistance(best.getDistance());
			house.setSegmentFrac(best.getSegmentFrac());
			house.setRoad(best.getRoad());
			house.setSegment(best.getSegment());
			for (HousenumberMatch altHouse : matches) {
				if (altHouse.getRoad() != best.getRoad() && altHouse.getDistance() < MAX_DISTANCE_TO_ROAD)
					house.addAlternativeRoad(altHouse.getRoad());
			}
				
			if (house.getRoad() == null) {
				house.setIgnored(true);
			} else {
				house.calcRoadSide();
			}
			// plausibility check for duplicate house numbers
			if (prev != null && prev.getHousenumber() == house.getHousenumber()
					&& prev.getSign().equals(house.getSign())) {
				// duplicate number (e.g. 10 and 10 or 10 and 10A or 10A and 10B)
				prev.setDuplicate(true);
				house.setDuplicate(true);
			}
			
			if (house.getRoad() == null && !house.isIgnored()) {
				log.warn("found no plausible road for house number element",house.toBrowseURL(),"(",streetName,house.getSign(),")");
			}
			if (!house.isIgnored()) {
				prev = house;
			}
		}
	}
	
	private static void markSimpleDuplicates(String streetName, List<HousenumberMatch> housesNearCluster) {
		List<HousenumberMatch> sortedHouses = new ArrayList<>(housesNearCluster);
		sortedHouses.sort(new HousenumberMatchByNumComparator());
		int n = sortedHouses.size();
		for (int i = 1; i < n; i++) {
			HousenumberMatch house1 = sortedHouses.get(i-1);
			if (house1.isIgnored())
				continue;
			HousenumberMatch house2 = sortedHouses.get(i);
			if (house2.isIgnored())
				continue;
			if (house1.getHousenumber() != house2.getHousenumber())
				continue;
			if (house1.getRoad() == house2.getRoad()) {
				if (house1.isFarDuplicate())
					house2.setFarDuplicate(true);
				continue; // handled later
			}
			// we have two equal house numbers in different roads
			// check if they should be treated alike
			boolean markFarDup = false;
			double dist = house1.getLocation().distance(house2.getLocation());
			if (dist > MAX_DISTANCE_SAME_NUM)
				markFarDup = true;
			else {
				CityInfo city1 = house1.getCityInfo();
				CityInfo city2 = house2.getCityInfo();
				if (city1 != null && !city1.equals(city2)) {
					markFarDup = true;
				}
			}
			if (markFarDup) {
				if (log.isDebugEnabled())
					log.debug("keeping duplicate numbers assigned to different roads in cluster", streetName, house1, house2);
				house1.setFarDuplicate(true);
				house2.setFarDuplicate(true);
				continue;
			}
			boolean ignore2nd = false;
			if (dist < 30) {
				ignore2nd = true;
			} else {
				Coord c1s = house1.getRoad().getPoints().get(house1.getSegment());
				Coord c1e = house1.getRoad().getPoints().get(house1.getSegment() + 1);
				Coord c2s = house2.getRoad().getPoints().get(house2.getSegment());
				Coord c2e = house2.getRoad().getPoints().get(house2.getSegment() + 1);
				if (c1s == c2s || c1s == c2e || c1e == c2s || c1e == c2e) {
					// roads are directly connected
					ignore2nd = true;
				} 
			}
			if (ignore2nd) {
				house2.setIgnored(true);
				if (log.isDebugEnabled()) {
					if (house1.getSign().equals(house2.getSign()))
						log.debug("duplicate number is ignored",streetName,house2.getSign(),house2.toBrowseURL() );
					else 
						log.info("using",streetName,house1.getSign(), "in favor of",house2.getSign(),"as target for address search");
				}
			} else {
				if (log.isDebugEnabled())
					log.debug("keeping duplicate numbers assigned to different roads in cluster", streetName, house1, house2);
				house1.setFarDuplicate(true);
				house2.setFarDuplicate(true);
			}
		}
	}


	public static void findClosestRoadSegment(HousenumberMatch house, MapRoad r) {
		findClosestRoadSegment(house, r, 0, r.getPoints().size());
	}
	
	/**
	 * Fill/overwrite the fields in house which depend on the assigned road.  
	 */
	public static void findClosestRoadSegment(HousenumberMatch house, MapRoad r, int firstSeg, int stopSeg) {
		Coord cx = house.getLocation();
		double oldDist = house.getDistance();
		MapRoad oldRoad = house.getRoad();
		house.setRoad(null);
		house.setDistance(Double.POSITIVE_INFINITY);
		boolean foundGroupLink = false;
		int end = Math.min(r.getPoints().size(), stopSeg+1);
		for (int node = firstSeg; node + 1 < end; node++) {
			Coord c1 = r.getPoints().get(node);
			Coord c2 = r.getPoints().get(node + 1);
			double frac = getFrac(c1, c2, cx);
			double dist = distanceToSegment(c1,c2,cx,frac);
			if (house.getGroup() != null && house.getGroup().linkNode == c1) {
				if (!c1.highPrecEquals(c2)) {
					log.debug("block doesn't have zero length segment! Road:",r,house);
				}
				foundGroupLink = true;
				house.setDistance(dist);
				house.setSegmentFrac(frac);
				house.setRoad(r);
				house.setSegment(node);
				break;
			} else if (dist < house.getDistance()) {
				house.setDistance(dist);
				house.setSegmentFrac(frac);
				house.setRoad(r);
				house.setSegment(node);
			} 
		}
		
		if (house.getGroup() != null && house.getGroup().linkNode != null && !foundGroupLink) {
			log.debug(r,house,"has a group but the link was not found, should only happen after split of zero-length-segment");
		}
		if (oldRoad == r && house.getDistance() > MAX_DISTANCE_TO_ROAD + 2.5 && oldDist <= MAX_DISTANCE_TO_ROAD) {
			log.warn("line distorted? Road segment was moved by more than", String.format("%.2f m", 2.5),
					", from address", r, house.getSign());
		}
	}

	private static boolean hasChanges(List<HousenumberRoad> housenumberRoads) {
		return housenumberRoads.stream().anyMatch(HousenumberRoad::isChanged);
	}

	/**
	 * 
	 * @param housenumberRoads
	 */
	private static void checkWrongRoadAssignmments(List<HousenumberRoad> housenumberRoads) {
		if (housenumberRoads.size() < 2)
			return;
		for (int loop = 0; loop < 10; loop++) {
			boolean changed = false;
			for (int i = 0; i+1 < housenumberRoads.size(); i++) {
				HousenumberRoad hnr1 = housenumberRoads.get(i);
				hnr1.setChanged(false);
				for (int j = i+1; j < housenumberRoads.size(); j++) {
					HousenumberRoad hnr2 = housenumberRoads.get(j);
					hnr2.setChanged(false);
					hnr1.checkWrongRoadAssignmments(hnr2);
					if (hnr1.isChanged()) {
						changed = true;
						hnr1.checkIntervals();
					}
					if (hnr2.isChanged()) {
						changed = true;
						hnr2.checkIntervals();
					}
				}
			}
			if (!changed)
				return;
		}
	}

	/**
	 * Sorts house numbers by roads, road segments and position of the house number.
	 * @author WanMil
	 */
	public static class HousenumberMatchByPosComparator implements Comparator<HousenumberMatch> {

		public int compare(HousenumberMatch o1, HousenumberMatch o2) {
			if (o1 == o2) {
				return 0;
			}
			if (o1.getRoad() == null || o2.getRoad() == null) {
				log.error("road is null in sort comparator", o1, o2);
				throw new MapFailedException("internal error in housenumber processing");
			}
			if (o1.getRoad() != o2.getRoad()) {
				// should not happen
				return o1.getRoad().getRoadId() - o2.getRoad().getRoadId();
			} 
			
			int dSegment = o1.getSegment() - o2.getSegment();
			if (dSegment != 0) {
				return dSegment;
			}
			
			double dFrac = o1.getSegmentFrac() - o2.getSegmentFrac();
			if (dFrac != 0d) {
				return (int)Math.signum(dFrac);
			}
			
			int d = o1.getHousenumber() - o2.getHousenumber();
			if (d != 0)
				return d;
			d = o1.getSign().compareTo(o2.getSign());
			if (d != 0)
				return d;
			return 0;
		}
		
	}
	
	/**
	 * Sorts house numbers by house number and segment 
	 * @author Gerd Petermann
	 */
	public static class HousenumberMatchByNumComparator implements Comparator<HousenumberMatch> {
		public int compare(HousenumberMatch o1, HousenumberMatch o2) {
			if (o1 == o2)
				return 0;
			int d = o1.getHousenumber() - o2.getHousenumber();
			if (d != 0)
				return d;
			d = o1.getSign().compareTo(o2.getSign());
			if (d != 0)
				return d;
			d = o1.getSegment() - o2.getSegment();
			if (d != 0)
				return d;
			double dDist = o1.getDistance() - o2.getDistance();
			if (dDist != 0d) {
				return (int)Math.signum(dDist);
			}
			return Long.compare(o1.getElement().getId(), o2.getElement().getId());
		}
	}
	/**
	 * Sorts house numbers by distance. If equal, compare segment and road to produce
	 * predictable results.  
	 * @author Gerd Petermann
	 */
	public static class HousenumberMatchByDistComparator implements Comparator<HousenumberMatch> {
		public int compare(HousenumberMatch o1, HousenumberMatch o2) {
			if (o1 == o2)
				return 0;
			int d = Double.compare(o1.getDistance(), o2.getDistance());
			if (d != 0)
				return d;
			d = Integer.compare(o1.getSegment(), o2.getSegment());
			if (d != 0)
				return d;
			d = Integer.compare(o1.getRoad().getRoadId(), o2.getRoad().getRoadId());
			if (d != 0)
				return d;
			return 0;
		}
	}
	
	/**
	 * If the closest point to a road is a junction, try to find the road
	 * segment that forms a right angle with the house 
	 * @param closestMatch one match that is closest to the node
	 * @param otherMatches  the other matches with the same distance
	 * @return the best match
	 */
	private static HousenumberMatch checkAngle(HousenumberMatch closestMatch,
			List<HousenumberMatch> otherMatches) {
		
		if (otherMatches.isEmpty())
			return closestMatch;
		HousenumberMatch bestMatch = closestMatch;
		for (HousenumberMatch alternative : otherMatches) {
			if (alternative == closestMatch)
				continue;
			if (closestMatch.getDistance() < alternative.getDistance())
				break;
			// a house has the same distance to different road objects
			// if this happens at a T-junction, make sure not to use the end of the wrong road 
			Coord c1 = closestMatch.getRoad().getPoints().get(closestMatch.getSegment());
			Coord c2 = closestMatch.getRoad().getPoints().get(closestMatch.getSegment()+1);
			Coord cx = closestMatch.getLocation();
			double dist = closestMatch.getDistance();
			double dist1 = cx.distance(c1);
			double angle;
			if (dist1 == dist)
				angle = Utils.getAngle(c2, c1, cx);
			else 
				angle = Utils.getAngle(c1, c2, cx);
			Coord c3 = alternative.getRoad().getPoints().get(alternative.getSegment());
			Coord c4 = alternative.getRoad().getPoints().get(alternative.getSegment()+1);
			
			double dist3 = cx.distance(c3);
			double altAngle;
			if (dist3 == dist)
				altAngle = Utils.getAngle(c4, c3, cx);
			else 
				altAngle = Utils.getAngle(c3, c4, cx);
			double delta = 90 - Math.abs(angle);
			double deltaAlt = 90 - Math.abs(altAngle);
			if (delta > deltaAlt) {
				bestMatch = alternative;
			}
		}
		if (log.isDebugEnabled()) {
			if (closestMatch.getRoad() != bestMatch.getRoad()) {
				log.debug("check angle: using road",bestMatch.getRoad().getRoadDef().getId(),"instead of",closestMatch.getRoad().getRoadDef().getId(),"for house number",bestMatch.getSign(),bestMatch.toBrowseURL());
			} else if (closestMatch != bestMatch) {
				log.debug("check angle: using road segment",bestMatch.getSegment(),"instead of",closestMatch.getSegment(),"for house number element",bestMatch.toBrowseURL());
			}
		}
		return bestMatch;
	}

	/**
	 * Evaluates if the given point lies on the left side of the line spanned by spoint1 and spoint2.
	 * @param spoint1 first point of line
	 * @param spoint2 second point of line
	 * @param point the point to check
	 * @return {@code true} point lies on the left side; {@code false} point lies on the right side
	 */
	public static boolean isLeft(Coord spoint1, Coord spoint2, Coord point) {
		if (spoint1.distance(spoint2) == 0) {
			log.warn("road segment length is 0 in left/right evaluation");
		}

		return ((spoint2.getHighPrecLon() - spoint1.getHighPrecLon())
				* (point.getHighPrecLat() - spoint1.getHighPrecLat()) - (spoint2
				.getHighPrecLat() - spoint1.getHighPrecLat())
				* (point.getHighPrecLon() - spoint1.getHighPrecLon())) > 0;
	}
	
	/**
	 * Calculates the distance to the given segment in meter.
	 * @param spoint1 segment point 1
	 * @param spoint2 segment point 2
	 * @param point point
	 * @param frac the fraction returned from {@code getFrac()} 
	 * @return the distance in meter
	 */
	public static double distanceToSegment(Coord spoint1, Coord spoint2, Coord point, double frac) {
		if (frac <= 0) {
			return spoint1.distance(point);
		} else if (frac >= 1) {
			return spoint2.distance(point);
		} else {
			return point.distToLineSegment(spoint1, spoint2);
		}
	}
	
	/**
	 * Calculates the fraction at which the given point is closest to 
	 * the infinite line going through both points.
	 * @param spoint1 segment point 1
	 * @param spoint2 segment point 2
	 * @param point point
	 * @return the fraction (can be <= 0 or >= 1 if the perpendicular is not on
	 * the line segment between spoint1 and spoint2) 
	 */
	public static double getFrac(Coord spoint1, Coord spoint2, Coord point) {
		int aLon = spoint1.getHighPrecLon();
		int bLon = spoint2.getHighPrecLon();
		int pLon = point.getHighPrecLon();
		int aLat = spoint1.getHighPrecLat();
		int bLat = spoint2.getHighPrecLat();
		int pLat = point.getHighPrecLat();
		
		double deltaLon = bLon - aLon;
		double deltaLat = bLat - aLat;

		if (deltaLon == 0 && deltaLat == 0) 
			return 0;
		else {
			// scale for longitude deltas by cosine of average latitude  
			double scale = Math.cos(Coord.hpToRadians((aLat + bLat + pLat) / 3) );
			double deltaLonAP = scale * (pLon - aLon);
			deltaLon = scale * deltaLon;
			if (deltaLon == 0 && deltaLat == 0)
				return 0;
			else 
				return (deltaLonAP * deltaLon + (pLat - aLat) * deltaLat) / (deltaLon * deltaLon + deltaLat * deltaLat);
		}
	}

	/**
	 * @param length
	 * @return string with length, e.g. "0.23 m" or "116.12 m"
	 */
	public static String formatLen(double length) {
		return String.format("%.2f m", length);
	}
	

	/**
	 * A performance critical part:
	 * Index all road segments to be able to find all road segments within a given range
	 * around a point.  
	 * @author Gerd Petermann
	 *
	 */
	private static class RoadSegmentIndex {
		private final KdTree<RoadPoint> kdTree = new KdTree<>();
		private final Int2ObjectOpenHashMap<Set<RoadPoint>> nodeId2RoadPointMap = new Int2ObjectOpenHashMap<>(); 
		private final double range;
		private final double maxSegmentLength;
		private final double kdSearchRange;

		private static class RoadPoint implements Locatable{
			final Coord p;
			final MapRoad r;
			final int segment;
			final int partOfSeg;
			
			public RoadPoint(MapRoad road, Coord co, int s, int part) {
				this.p = co;
				this.r = road;
				this.segment = s;
				this.partOfSeg = part;
			}
			@Override
			public Coord getLocation() {
				return p;
			}
			@Override
			public String toString() {
				return r + " " + segment + " " + partOfSeg;
			}
		}

		public RoadSegmentIndex(List<MapRoad> roads, double rangeInMeter) {
			this.range = rangeInMeter;
			this.maxSegmentLength = range * 2 / 3;
			this.kdSearchRange = Math.sqrt(Math.pow(rangeInMeter, 2) + Math.pow(maxSegmentLength/2, 2));
			build(roads);
			
		}

		public void build(List<MapRoad> roads) {
			for (MapRoad road : roads) {
				if (road.isSkipHousenumberProcessing())
					continue;
				List<Coord> points = road.getPoints();
				if (points.size() < 2)
					continue;

				List<RoadPoint> roadPoints = new ArrayList<>();
				RoadPoint rp;
				for (int i = 0; i + 1 < points.size(); i++) {
					Coord c1 = points.get(i);
					Coord c2 = points.get(i + 1);
					int part = 0;
					rp = new RoadPoint(road, c1, i, part++);
					roadPoints.add(rp);
					while (true) {
						double segLen = c1.distance(c2);
						double frac = maxSegmentLength / segLen;
						if (frac >= 1)
							break;
						// if points are not close enough, add extra point
						c1 = c1.makeBetweenPoint(c2, frac);
						rp = new RoadPoint(road, c1, i, part++);
						roadPoints.add(rp);
					}
				}
				int last = points.size() - 1;
				rp = new RoadPoint(road, points.get(last) , last, -1);
				roadPoints.add(rp);
				
				Collections.shuffle(roadPoints);
				for (RoadPoint toAdd : roadPoints) {
					int id = toAdd.p.getId();
					if (id == 0)
						kdTree.add(toAdd);
					else {
						// Coord node, add only once to KD-tree with all roads
						Set<RoadPoint> set = nodeId2RoadPointMap.get(id);
						if (set == null) {
							set = new LinkedHashSet<>();
							nodeId2RoadPointMap.put(id, set);
							kdTree.add(toAdd);
						}
						set.add(toAdd);
					}  		
				}
			}
		}
		
		public List<RoadPoint> getCLoseRoadPoints(HousenumberElem house) {
			Set<RoadPoint> closeRoadPoints = kdTree.findClosePoints(house, kdSearchRange);
			List<RoadPoint> result = new ArrayList<>();
			for (RoadPoint rp : closeRoadPoints) {
				int id = rp.p.getId();
				if (id != 0)
					result.addAll(nodeId2RoadPointMap.get(id));
				else 
					result.add(rp);
			}
			return result;
		}
		
		/**
		 * Find closest road segment and other plausible roads for a house
		 * @param house
		 * @return null if no road was found, else a {@link HousenumberMatch} instance 
		 */
		public HousenumberMatch createHousenumberMatch(HousenumberElem house) {
			HousenumberMatch closest = new HousenumberMatch(house);
			List<RoadPoint> closeRoadPoints = getCLoseRoadPoints(house);
			if (closeRoadPoints.isEmpty())
				return closest;
			closeRoadPoints.sort((o1,o2)-> {
					if (o1 == o2)
						return 0;
					int d = Integer.compare(o1.r.getRoadId(), o2.r.getRoadId());
					if (d != 0)
						return d;
					d = Integer.compare(o1.segment, o2.segment);
					if (d != 0)
						return d; 
					return Integer.compare(o1.partOfSeg, o2.partOfSeg);
				}
			);

			List<HousenumberMatch> matches = new ArrayList<>(40);
			BitSet testedSegments = new BitSet();
			MapRoad lastRoad = null;
			HousenumberMatch hnm = null;
			for (RoadPoint rp : closeRoadPoints) {
				if (house.getStreet() != null && rp.r.getStreet() != null
						&& !house.getStreet().equalsIgnoreCase(rp.r.getStreet())) {
					// we have a given street name, accept only roads with similar name or no name
					continue;
				}
				if (rp.r != lastRoad) {
					hnm = new HousenumberMatch(house);
					testedSegments.clear();
					matches.add(hnm);
					lastRoad = rp.r;
				}
				if (rp.partOfSeg >= 0 && !testedSegments.get(rp.segment)) {
					// rp.p is at start or before end of segment
					testedSegments.set(rp.segment);
					checkSegment(hnm, rp.r, rp.segment);
				}
				if (rp.partOfSeg < 0) {
					// rp is at end of road, check (also) the preceding segment
					if (rp.segment < 1) {
						log.error("internal error: trying to use invalid roadPoint", rp);
					} else if (!testedSegments.get(rp.segment - 1)) {
						testedSegments.set(rp.segment - 1);
						checkSegment(hnm, rp.r, rp.segment - 1);
					}
				}
			}
			if (matches.isEmpty())
				return closest; // closest has not yet a road
			
			matches.sort(new HousenumberGenerator.HousenumberMatchByDistComparator());
			closest = matches.get(0);
			closest = checkAngle(closest, matches);
			closest.calcRoadSide();
			HousenumberMatch bestMatchingName = null; 
			if (closest.getStreet() != null && closest.getStreet().equalsIgnoreCase(closest.getRoad().getStreet()))
				bestMatchingName = closest;
			
			for (HousenumberMatch altHouse : matches) {
				if (altHouse.getDistance() >= MAX_DISTANCE_TO_ROAD)
					break;
				if (altHouse.getRoad() != closest.getRoad()) {
					if (house.getStreet() != null && altHouse.getDistance() > closest.getDistance()) {
						if (house.getStreet().equalsIgnoreCase(altHouse.getRoad().getStreet())) {
							if (bestMatchingName == null || bestMatchingName.getDistance() > altHouse.getDistance()) {
								bestMatchingName = altHouse;
							}
						} else {
							if (bestMatchingName != null && altHouse.getDistance() > bestMatchingName.getDistance())
								continue;
						}
					}
					closest.addAlternativeRoad(altHouse.getRoad());
				}
			}
			if (bestMatchingName != null && !house.getStreet().equals(bestMatchingName.getRoad().getStreet())) {
				log.warn("accepting match in spite of different capitalisation", house.getStreet(), house.getSign(),
						bestMatchingName.getRoad().getRoadDef(), "house:", house.toBrowseURL());
				bestMatchingName.setStreet(bestMatchingName.getRoad().getStreet());
				closest.setStreet(bestMatchingName.getStreet());
			}
			if (closest == bestMatchingName || bestMatchingName == null || bestMatchingName.getDistance() > MAX_DISTANCE_TO_ROAD)
				return closest;
			
			double ratio = closest.getDistance() / bestMatchingName.getDistance();
			if (ratio < 0.25)
				return closest;
			HousenumberMatch best = closest;
			if (ratio > 0.75) {
				// prefer the road with the matching name
				for (MapRoad r : closest.getAlternativeRoads()) {
					if (house.getStreet().equalsIgnoreCase(r.getStreet()))
						bestMatchingName.addAlternativeRoad(r);
				}
				best = bestMatchingName;
				best.calcRoadSide();
			} else {
				if (log.isDebugEnabled()) {
					log.debug("further checks needed for address", closest.getStreet(), closest.getSign(), closest.toBrowseURL(), 
							formatLen(closest.getDistance()), formatLen(bestMatchingName.getDistance()));
				}

			}
			return best;
		}

		private static void checkSegment(HousenumberMatch house, MapRoad road, int seg) {
			Coord cx = house.getLocation();
			Coord c0 = road.getPoints().get(seg);
			Coord c1 = road.getPoints().get(seg + 1);
			double frac = getFrac(c0, c1, cx);
			double dist = distanceToSegment(c0,c1,cx,frac);
			if (dist < house.getDistance()) {
				house.setDistance(dist);
				house.setRoad(road);
				house.setSegment(seg);
				house.setSegmentFrac(frac);
			}
		}
	}
}


