#!/usr/bin/perl -w
# -*- perl -*-

#
# Author: Slaven Rezic
#
# Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2023 Slaven Rezic. All rights reserved.
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
# Mail: slaven@rezic.de
# WWW:  http://www.rezic.de/eserte/
#

use strict;
use FindBin;
use lib "$FindBin::RealBin/..";

use vars qw($VERSION);
$VERSION = '0.40';

use Cwd qw(getcwd realpath);
use Data::Dumper;
use File::Basename qw(basename);
use File::Spec qw();
use Getopt::Long;
use IO::File;
use List::Util qw(max);
use POSIX qw(strftime);

sub epoch2isodatez (;$);
sub touch ($);

use constant DO_ASSERTS => 1;
use constant SORTED_TAGS => 1;
use constant SORTED_ELEMENTS => 1;
if (SORTED_TAGS || SORTED_ELEMENTS) {
    require Tie::IxHash;
    # See http://rt.cpan.org/Ticket/Display.html?id=39619
    if (!defined &Tie::IxHash::SCALAR) {
	*Tie::IxHash::SCALAR = sub {
	    scalar @{ $_[0]->[1] };
	};
    }
    if (!defined &Tie::IxHash::CLEAR) {
	if (!defined &Tie::IxHash::Clear) { # Clear() appeared in Tie::IxHash 1.23
	    *Tie::IxHash::Clear = sub {
		my $s = shift;
		$s->[0] = {};   # hashkey index
		$s->[1] = [];   # array of keys
		$s->[2] = [];   # array of data
		$s->[3] = 0;    # iter count
		return;
	    };
	}
	*Tie::IxHash::CLEAR = \&Tie::IxHash::Clear;
    }
}

# Same as in Strassen::Core, which is coincidence.
use constant STRASSEN_NAME   => 0;
use constant STRASSEN_COORDS => 1;
use constant STRASSEN_CAT    => 2;

my $o;
my $f;
my $v;
my $encoding = "utf-8";
my $debug = 1; # 0: quiet, 1: normal, 2: verbose
my $ignore_unhandled;
my $ignore_underline;

my $nodate;
my $map;
my $center_wgs84;
my $center_delta;

my $parsefor;
my $country = '';
my $lang = '';
use constant MAXLINELENGTH => 12288; # the limit of BBBikeXS
my $splitlonglines = 1; # XXX this may change once BBBikeXS has no length restriction anymore
my $xmlparser;
my %height_db;
my $height_db_file;
my $use_height_db = 0;
my $no_create;
my $handle_relations = 1;
my $do_fingerprint;

my %experiments;
my %known_experiments = map {($_,1)} qw(add_postal_code coastline_hack polar_coord_hack handle_relations);

sub usage () {
    <<EOF;
usage: $0 [-v] [-f] [-encoding enc] [-center lon,lat] [-map bbbike [-centerdelta lon,lat]] [-country country] -o directory osmfile ...

-v                Show parser progress
-f                Force overwriting existing output directory
-enc ...          Specify different encoding, standard is utf-8
-map bbbike       Use BBBike coordinates instead of WGS 84 coordinates in the
                  output. Recommended until BBBike is able to fully handle
                  WGS 84 coordinates.
-center lon,lat   Set the center of the map. If not set, then the plotting
		  software decides on a center itself, usually the center
		  of the bounding box.
-centerdelta lon,lat If -map bbbike is used, then specify the 0,0 point.
		     Defaults to standard Berlin setting.
-o directory      A non-existing directory to output the bbd files.
-parsefor ...     Parse only for the given key=value pair,
                  key as string and val as regexp
-country ...      Optimize output for given country (see documentation for
                  supported countries)
-lang ...         Optimize output for given language (see documentation for
                  supported languages)
-ignore-unhandled do not create the _unhandled layer
-ignore-underline do not create any of the "_*" layers
-experiment ...   Turn on experimental features (may be given multiple times).
-height-db=file   read height values from database. Same format as ./hoehe
osmfile ...       Files downloaded by downloadosm,
                  or directory containing .osm files
-no-create	  do not store create date and cwd
-no-handle-relations Skip parsing and handling relations
                     (e.g. for memory reasons)
-fingerprint-only Print fingerprint from commandline and input files contents and exit
-version          Show version and exit
EOF
}

my $prog = realpath $0;
my @commandline = ($^X, $0, @ARGV);
my $cwd = getcwd;

GetOptions("o=s" => \$o,
	   "f" => \$f,
	   "v" => \$v,
	   "no-create" => \$no_create,
	   "debug=i" => \$debug,
	   "map=s" => \$map,
	   "height-db=s" => \$height_db_file,
	   "center=s" => \$center_wgs84,
	   "centerdelta=s" => \$center_delta,
	   "encoding=s" => \$encoding,
	   "parsefor=s" => \$parsefor,
	   "country=s" => \$country,
	   "lang=s" => \$lang,
	   "nodate" => \$nodate,
	   "splitlonglines!" => \$splitlonglines,
	   "xmlparser=s" => \$xmlparser,
	   "ignore-unhandled!" => \$ignore_unhandled,
	   "ignore-underline!" => \$ignore_underline,
	   "handle-relations!" => \$handle_relations,
	   'experiment=s@' => sub {
	       my $exp = $_[1];
	       if (!$known_experiments{$exp}) {
		   die <<EOF;
Unknown experiment '$exp'. Known experiments are:
@{[ sort keys %known_experiments ]}
(handle_relations is actually set on by default)
EOF
	       }
	       $experiments{$exp}++;
	   },
	   'fingerprint-only' => \$do_fingerprint,
	   'version' => sub {
	       print basename($0) . " $VERSION\n";
	       exit 0;
	   },
	  )
    or die usage;

my @osm_files = @ARGV;
if (!@osm_files) {
    die <<EOF . "\n" . usage;
Please specify one or more osm files. To download an osm file, you can use
the script downloadosm (usually to be found in the same directory as this
script) or raw wget, for example

  wget -O filename.osm https://api.openstreetmap.org/api/0.5/map?bbox=x0,y0,x1,y1

Note that the bbox must not be too large, otherwise you get a 400 bad request
error.
EOF
}

if ($do_fingerprint) {
    fingerprint_and_exit();
}

init_height_db($height_db_file)
    if $height_db_file;

my $newest_mtime = max map { (stat($_))[9] } @osm_files;

my $today_date = strftime "%Y-%m-%d", localtime;

if ($center_wgs84 && $center_wgs84 !~ m{^[-+]?[\d\.]+,[-+]?[\d\.]+$}) {
    die "The value for the -center option has to be in the form lon,lat\n";
}

my($parsefor_key, $parsefor_val);
if ($parsefor) {
    if (!(($parsefor_key, $parsefor_val) = $parsefor =~ m{^(.*)=(.*)$})) {
	die "-parsefor value <$parsefor> must be in the form key=val";
    }
    $parsefor_val = qr{$parsefor_val};
}

if (!$parsefor) {
    if (!$o) {
	die "Please specify (non-existent) output directory with -o option.\n" . usage;
    }
    if (!$f && -e $o) {
	die "Output directory <$o> must not exist (or specify -f to force overwrite).\n";
    }
}

# fallback
sub M ($) { $_[0] }
sub Mfmt  { sprintf(shift, @_) }

if ($lang) {
    $lang = '' if $lang eq 'de';
}
if ($lang) {
    die "Language '$lang' is not supported, please specify either 'de' or 'en'" unless $lang eq 'en';
}

my %unhandled;
my %out;
my %resolve_node_later;
my @bbox_wgs84;
my %global_directives;
my %place_to_info;
my %additional_data_per_file;
my %addr;
tie %addr, 'Tie::IxHash' if SORTED_ELEMENTS;

# XXX see $splitlonglines
# As fast_plot_str() is used only for layer types
# 's' und 'l' (streets and country streets), 
# the $splitlonglines hack may be used only for a limited
# set of output files.
my %file_needs_splitlonglines = map { ($_,1) } qw(strassen landstrassen landstrassen2);

%global_directives =
    (
     "_building"     => ["layer_stack: above:i"],
     "_motortraffic" => ["category_image.Parking: ../misc/verkehrszeichen/Zeichen_314.svg:24x24=1:3000,xxx"],
     "_oepnv"	     => ['category_color.Busstop: #7d176b',
			 'category_color.Bus: #7d176b',
			 'category_color.Tramstop: red',
			],
     "borders" => ["line_color: #000000", # per bbd.pod
		   "str_color: #000000", # in reality
		   # Z4: Landesgrenzen
		   # Z5: gibt's in NRW
		   (map { "line_dash.Z$_: 8,5,2,5" } (1 .. 10)),
		  ],
     'zebrastreifen' => ['category_image.Zs: ../misc/verkehrszeichen/Zeichen_350.svg:24x24=1:12000,xxx'],
     'grenzuebergaenge' => ['category_image.GU: grenzuebergang_16'],
     'ortsschilder' => ['category_image.OS: ../misc/verkehrszeichen/Zeichen_310_leer.svg:24x24=1:12000,xxx',
			'title: Ortsschilder'],
     'radwege_exact' => ['line_do_offset: 1',
			 'line_arrow: none'],
    );

# see http://www.mail-archive.com/talk@openstreetmap.org/msg06041.html
# and http://wiki.openstreetmap.org/wiki/Key:tracktype
my %grade_to_Q = (1 => "Q0",
		  2 => "Q1",
		  3 => "Q2",
		  4 => "Q3",
		  5 => "Q3-",
		 );
# XXX to be reviewed
my %smoothness_to_Q = ('excellent'     => 'Q0',
		       'good'          => 'Q1',
		       'intermediate'  => 'Q2',
		       'bad'           => 'Q3',
		       'very_bad'      => 'Q3-',
		       'horrible'      => 'Q3--',
		       'very_horrible' => 'Q3---',
		       'impassable'    => undef, # will be turned into a 'gesperrt, 2'
		      );		       

my %smoothness_to_de = ('excellent'     => 'ausgezeichnet',
			'good'          => 'gut',
			'intermediate'  => 'mig',
			'bad'           => 'schlecht',
			'very_bad'      => 'sehr schlecht',
			'horrible'      => 'grauenhaft',
			'very_horrible' => 'sehr grauenhaft',
			'impassable'    => 'unpassierbar',
		       );		       

use constant HIGHWAY_BAB_RX => qr{^(?:motorway|motorway_link)$};
use constant HIGHWAY_TRUNK_RX => qr{^(?:trunk|trunk_link)$}; # may be rendered as motorroad or as primary
use constant HIGHWAY_HH_RX  => qr{^(?:primary|primary_link)$};
use constant HIGHWAY_H_RX   => qr{^(?:secondary|secondary_link)$};
use constant HIGHWAY_NH_RX  => qr{^(?:tertiary|tertiary_link)$};
use constant HIGHWAY_N_RX   => qr{^(?:residential|unclassified|service|minor)$};

use constant CONSTRUCTION_RX => qr{^(?:construction)$};
use constant PROJECTED_RX    => qr{^(?:planned|proposed)$};

use constant BRIDGE_YES_RX  => qr{^(?:yes|movable)$};

my $cyclists_dismount_label = $lang eq 'de' ? 'Radfahrer absteigen' : 'Cyclists dismount';
my $incline_label           = $lang eq 'de' ? 'Steigung' : 'Incline';

sub init_height_db {
    my $file = shift;

    my $fd = new IO::File $file, "r"
	or die "open '$file': $!\n";

    my $use_rounding = 2;

    while(<$fd>) {
	chomp;
	my ($height, $x, $coord) = split;

	if ($use_rounding == 1) {
	    $height = int($height + 0.5);
	} elsif ($use_rounding == 2) {
	    $height = int(10*$height + 0.5)/10;
	}
     
	$height_db{$coord} = $height;
    }
    close $fd;

    $use_height_db = 1;
}

sub add_underline {
    return if $ignore_underline;
    my($or_ref, $name, $cat) = @_;
    push @$or_ref, [$name, $cat];
}

my $cemetery_tag = sub {
    my($tagref) = @_;
    if ($tagref->{religion} && $tagref->{religion} =~ m{^(muslim|jewish)$}) {
	"F:Cemetery|religion=$tagref->{religion}";
    } else {
	"F:Cemetery";
    }
};

my $do_amenity = sub {
    my($is_area, $amenity, $tagref) = @_;
    my @or;
    my $F = $is_area ? "F:" : "";
    if ($amenity eq 'place_of_worship') {
	my $religion = $tagref->{'religion'};
	if (!$religion || $religion eq 'christian') {
	    push @or, ["sehenswuerdigkeit", $F."SW|IMG:church"];
	} elsif ($religion eq 'jewish') {
	    push @or, ["sehenswuerdigkeit", $F."SW|IMG:synagogue"];
	} elsif ($religion eq 'muslim') {
	    push @or, ["sehenswuerdigkeit", $F."SW|IMG:mosque"];
	} else {
	    $unhandled{"amenity-religion-$religion"}++;
	}
    } elsif ($amenity eq 'museum') {
	push @or, ["sehenswuerdigkeit", $F."SW|IMG:museum"];
    } elsif ($amenity eq 'hospital') {
	push @or, ["sehenswuerdigkeit", $F."SW|IMG:hospital"];
    } elsif ($amenity =~ m{^theat(?:er|re)$}) {
	push @or, ["sehenswuerdigkeit", $F."SW|IMG:theater"];
    } elsif ($amenity =~ m{^(?:shopping|shopping_centre)$}) {
	push @or, ["sehenswuerdigkeit", $F."Shop"];
    } elsif ($amenity =~ m{^(?:arts_center|courthouse|library|police|post_office|public_building|townhall|university|embassy|prison)$}) {
	push @or, ["sehenswuerdigkeit", $F."SW"];
    } elsif ($amenity =~ m{^(?:biergarten|cafe|cafeteria|pub|bar|ice_cream)$}) {
	push @or, ["kneipen", $F."X"];
    } elsif ($amenity =~ m{^(?:fast_food|restaurant)$}) {
	push @or, ["restaurants", $F."X"];
    } elsif ($amenity eq 'cinema') {
	push @or, ["kinos", $F."X"];
    } elsif ($amenity =~ m{^(?:kindergarten|school)$}) {
	add_underline(\@or, "_education", $F."X");
    } elsif ($amenity eq 'parking') {
	my $cat = ($F 
		   ? $F."#f0f0f0|IMG:Parking" # XXX Note: Perl/Tk cannot handle IMG:Parking here (category_image mapping not used)
		   : "Parking"
		  );
	add_underline(\@or, "_motortraffic", $cat);
    } elsif ($amenity eq 'car_rental') {
	add_underline(\@or, "_motortraffic", 'X');
    } elsif ($amenity eq 'border_control') {
	push @or, ['grenzuebergaenge', 'GU'];
    } elsif ($amenity eq 'grave_yard') {
	if ($is_area) {
	    push @or, ['sehenswuerdigkeit', $cemetery_tag->($tagref)];
	} else {
	    push @or, ['sehenswuerdigkeit', 'SW'];
	}
    } else {
	$unhandled{"amenity-$amenity"}++;
    }
    @or;
};

my $do_historic = sub {
    my($is_area, $historic) = @_;
    my @or;
    my $F = $is_area ? "F:" : "";
    if ($historic eq 'monument') {
	push @or, ["sehenswuerdigkeit", $F."SW|IMG:monument"];
    } elsif ($historic =~ m{^(?:castle|memorial|archaeological_site|ruins)$}) {
	push @or, ["sehenswuerdigkeit", $F."SW"];
    } else {
	$unhandled{"historic-$historic"}++;
    }
    @or;
};

my $do_tourism = sub {
    my($is_area, $tourism) = @_;
    my @or;
    my $F = $is_area ? "F:" : "";
    if ($tourism eq 'attraction') {
	push @or, ["sehenswuerdigkeit", $F."SW"];
    } elsif ($tourism eq 'museum') {
	push @or, ["sehenswuerdigkeit", $F."SW|IMG:museum"];
    } elsif ($tourism =~ m{^(hotel|hostel|motel|guest_house)$}) {
	push @or, ["sehenswuerdigkeit", $F."SW|IMG:hotel"];
    } else {
	$unhandled{"tourism-$tourism"}++;
    }
    @or;
};

my $do_fixme = sub {
    my($tagref) = @_;
    my @or;
    if (exists $tagref->{fixme}) {
	push @or, ['fragezeichen', '?', name => $tagref->{fixme}];
    } elsif (exists $tagref->{note} && $tagref->{note} =~ m{^fixme\s*(.*)}i) {
	push @or, ['fragezeichen', '?', name => $1];
    }
    @or;
};

# Return "ignore_unhandled" flag
my $do_addr = sub ($$$) {
    my($tagref, $nodestype, $nodesval) = @_;
    if (exists $tagref->{'addr:city'} && exists $tagref->{'addr:housenumber'} &&
	exists $tagref->{'addr:postcode'} && exists $tagref->{'addr:street'}) {
	my $name = join("|", @{$tagref}{qw(addr:street addr:housenumber addr:postcode addr:city)});
	if (!exists $addr{$name} || ($tagref->{entrance}||'') eq 'yes') { # prefer elements with entrance=yes
	    $addr{$name} = {name => $name, $nodestype => $nodesval};
	}
	if (exists $tagref->{'amenity'}) {
	    0;
	} else {
	    1;
	}
    } else {
	0;
    }
};

my $do_unhandled = sub {
    my($name_ref, $tagref) = @_;

    for my $key (keys %$tagref) {
	next if $key =~ m{^(?:
			    name
			  | created_by
			  | source
			  | url
			 )$}x;
	my $val = $tagref->{$key};
	if ($$name_ref) { $$name_ref .= "; " }
	$$name_ref .= "$key:$val";
    }
};

{
    # expand directories
    # XXX Should be really a recursive function, probably
    my @new_osm_files;
    for my $osm_file (@osm_files) {
	if (-d $osm_file) {
	    push @new_osm_files, grep { -f $_ && -s $_ } glob(File::Spec->catfile($osm_file,"*.{osm,osm.gz,osm.bz2}")); # XXX argh, grep duplicates functionality, see "recursive" suggestion above
	} elsif (-z $osm_file) {
	    # May happen while loading data, so ignore this
	    warn "Ignore empty file <$osm_file>...\n";
	} else {
	    push @new_osm_files, $osm_file;
	}
    }
    @osm_files = @new_osm_files;
}

# Create a "url" directive, if applicable. Return true if such a
# directive was actually created.
my $set_url_directive = sub {
    my($tags_ref, $out_ref) = @_;
    die "Cannot autovivify out ref, please make sure it's an initialized array ref in the caller!" if !$out_ref;

    # XXX evtl. die folgenden Direktiven nur im
    # "Hauptfile" ausgeben, siehe "width" unten?
    ## Also, it's not lear how to handle all the possible languages. Now
    ## the compromise is to use the english entry and the language specified
    ## while doing the conversion. Not ideal.
    for my $key (qw(wikipedia website url contact:website)) {
	my $url;
	for my $_uselang ($lang, '', 'en', 'de') {
	    my $suffix = $_uselang ne '' ? ":$_uselang" : '';
	    my $uselang = $_uselang ne '' ? $_uselang : 'en';
	    my $key_suffix = "$key$suffix";
	    if (exists $tags_ref->{$key_suffix}) {
		if ($key eq 'wikipedia' && $tags_ref->{$key_suffix} !~ m{^http:}) {
		    $url = "http://$uselang.wikipedia.org/wiki/" . $tags_ref->{$key_suffix};
		} else {
		    $url = $tags_ref->{$key_suffix};
		}
	    }
	    if ($url) {
		push @$out_ref, ["#: url: $url"];
		return 1;
	    }
	}
    }
    return 0;
};

# Conversion function
my $conv = sub {
    my($lon,$lat) = @_;
    "$lon,$lat";
};
if (!$experiments{'polar_coord_hack'}) {
    if ($o) {
	if (-e "$o/Karte/Polar.pm") {
	    unlink "$o/Karte/Polar.pm"
		or die "Cannot remove $o/Karte/Polar.pm: $!";
	}
    } # output directory may be missing in -parsefor operation
}
if ($experiments{'polar_coord_hack'}) {
    require Math::Trig;
    my $lat;
    if ($center_wgs84) {
	(undef, $lat) = split /,/, $center_wgs84;
    } else {
	my($min_lat, $max_lat);
	# XXX taken from BBBikeOsmUtil.pm
	my $osm_download_file_qr = qr{/download_(-?\d+\.\d+),(-?\d+\.\d+),(-?\d+\.\d+),(-?\d+\.\d+)\.osm(?:\.gz|\.bz2)?$};
	# XXX it would be better to read the bboxes of all XML files
	for my $osm_file (@osm_files) {
	    my($this_min_lat, $this_max_lat);
	    if ($osm_file =~ $osm_download_file_qr) {
		($this_min_lat, $this_max_lat) = ($2, $4);
	    } else {
		(undef, $this_min_lat, undef, $this_max_lat) = eval { get_bounds_from_osm_file($osm_file) };
		warn "WARNING: cannot get bounds from $osm_file: $@" if $@;
	    }
	    if (defined $this_min_lat && (!defined $min_lat || $this_min_lat < $min_lat)) {
		$min_lat = $this_min_lat;
	    }
	    if (defined $this_max_lat && (!defined $max_lat || $this_max_lat > $max_lat)) {
		$max_lat = $this_max_lat;
	    }
	}
	if (defined $min_lat && defined $max_lat) {
	    $lat = ($max_lat - $min_lat)/2 + $min_lat;
	}
    }
    if (!defined $lat) {
	die <<EOF;
Cannot apply polar_coord_hack experiment, because the center could not
be determined. Most commonly this happens because the
osm file does not have a <bounds> tag (e.g. because it was created
using osmosis). To fix this, set manually the -center option.
EOF
	# XXX should read the bboxes of all files
    }
    my $newx1 = cos(Math::Trig::deg2rad($lat))*111_000; # XXX 111km is rough for one degree

    mkdir $o if !-d $o; # does not exist yet, see below the OUTPUT section
    mkdir "$o/Karte" if !-d "$o/Karte";
    open my $ifh, "<", "$FindBin::RealBin/../Karte/Polar.pm"
	or die "Cannot open $FindBin::RealBin/../Karte/Polar.pm: $!";
    open my $ofh, ">", "$o/Karte/Polar.pm~"
	or die "Cannot write to $o/Karte/Polar.pm~: $!";
    print $ofh q{warn "Using corrected Karte::Polar for latitude } . $lat . q{...\n";} . "\n";
    while(<$ifh>) {
	s{^(\s*X1\s*=>\s*)[0-9.-]+}{$1$newx1};
	print $ofh $_;
    }
    close $ofh
	or die "Error while writing to Polar.pm~: $!";
    rename "$o/Karte/Polar.pm~", "$o/Karte/Polar.pm"
	or die "Error while renaming Polar.pm~: $!";

    lib->import($o); # so the hacked Karte::Polar is in front of @INC
}

if ($map) {
    if ($map ne 'bbbike') {
	die "Only -map bbbike is supported.\n";
    }
    require Karte::Polar;
    require Karte::Standard;

    my($dx,$dy) = (0,0);
    if ($center_delta) {
	my($c_lon,$c_lat) = split /,/, $center_delta;
	($dx,$dy) = $Karte::Standard::obj->trim_accuracy($Karte::Polar::obj->map2standard($c_lon,$c_lat));
    }

    $conv = sub {
	my($lon,$lat) = @_;
	my($x,$y) = $Karte::Standard::obj->trim_accuracy($Karte::Polar::obj->map2standard($lon,$lat));
	($x-$dx).",".($y-$dy);
    };
}

my $tp;
if (@osm_files > 1) {
    eval {
	die "No terminal" if !is_interactive();
	require Time::Progress;
	$tp = Time::Progress->new;
	$tp->attr(min => 0, max => 2 * $#osm_files + 1);
	$tp->restart;
	if ($handle_relations) {
	    print STDERR "Three passes are following: nodes, ways, and relations\n";
	} else {
	    print STDERR "Two passes are following: nodes and ways\n";
	}
    };
}

sub padding {
    my $lat = shift;

    my ($int, $dec) = split(/\./, $lat);
    
    while(length($dec) < 7) {
	$dec .= '0';
	$lat .= '0';
    }
    return $lat;
}

######################################################################
# NODES
my %node2ll;
my $osm_file_i = 0;
for my $osm_file (@osm_files) {
    if ($v) {
	if ($tp) {
	    print STDERR $tp->report("\r  nodes done %p elapsed: %L, ETA %E (" . substr(basename($osm_file),0,28) . ")", $osm_file_i++);
	} else {
	    warn "Parse $osm_file for nodes...\n";
	}
    }
    my($dom, $reader) = open_osm($osm_file);
    set_info_handler($osm_file, $dom, $reader);

 PARSE: {

	if ($reader) {
	    $reader->nextElement;
	    die "The file '$osm_file' is not starting with a <osm> tag, probably not an osm file?"
		if $reader->name ne 'osm';
	    $reader->nextElement == 1 or last PARSE;
	    # Overpass API generated osm files have a <note> element
	    if ($reader->name eq 'note') {
		$reader->nextElement == 1 or last PARSE;
	    }
	    # ... and a <meta> element
	    if ($reader->name eq 'meta') {
		$reader->nextElement == 1 or last PARSE;
	    }
	    # $reader stays now on <bounds> or <bound> or the first <node>
	    if (DO_ASSERTS) {
		$reader->name =~ m{^(?:bounds|bound|node)}
		    or parse_warning($reader, "after parsing <osm>");
	    }
	}

	if ($reader && $reader->name eq 'bound') { # is this the new api? <bound> vs. <bounds>?
	    # XXX missing implementation for XML::LibXML-non-reader and <bound> tag
	    my($minlat,$minlon,$maxlat,$maxlon) = split /,/, $reader->getAttribute('box');
	    if (!defined $bbox_wgs84[0] || $minlon < $bbox_wgs84[0]) { $bbox_wgs84[0] = $minlon }
	    if (!defined $bbox_wgs84[1] || $minlat < $bbox_wgs84[1]) { $bbox_wgs84[1] = $minlat }
	    if (!defined $bbox_wgs84[2] || $maxlon > $bbox_wgs84[2]) { $bbox_wgs84[2] = $maxlon }
	    if (!defined $bbox_wgs84[3] || $maxlat > $bbox_wgs84[3]) { $bbox_wgs84[3] = $maxlat }

	    $reader->nextElement == 1 or last PARSE;
	} else {
	    # XXX It seems that the osm specification allows multiple
	    # bounds elements, but the real-existing osm files only
	    # have one, so check only for one:
	    my $bounds_node;
	    if ($reader) {
		if ($reader->name eq 'bounds') {
		    $bounds_node = $reader;
		}
	    } else {
		($bounds_node) = $dom->findnodes("/osm/bounds");
	    }

	    if ($bounds_node) {
		my $minlat = $bounds_node->getAttribute("minlat");
		my $maxlat = $bounds_node->getAttribute("maxlat");
		my $minlon = $bounds_node->getAttribute("minlon");
		my $maxlon = $bounds_node->getAttribute("maxlon");
		if (!defined $bbox_wgs84[0] || $minlon < $bbox_wgs84[0]) { $bbox_wgs84[0] = $minlon }
		if (!defined $bbox_wgs84[1] || $minlat < $bbox_wgs84[1]) { $bbox_wgs84[1] = $minlat }
		if (!defined $bbox_wgs84[2] || $maxlon > $bbox_wgs84[2]) { $bbox_wgs84[2] = $maxlon }
		if (!defined $bbox_wgs84[3] || $maxlat > $bbox_wgs84[3]) { $bbox_wgs84[3] = $maxlat }

		if ($reader) {
		    $reader->nextElement == 1 or last PARSE;
		}
	    }
	}

	if ($reader) {
	    # $reader stays now on the first <node> (or on
	    # another <bounds>, if there is really one)
	    if (DO_ASSERTS) {
		$reader->name eq 'node' or
		    parse_warning($reader, "after parsing <bounds> or <bound>");
	    }
	}

	my @nodes;
	if ($dom) {
	    @nodes = $dom->findnodes('/osm/node');
	}

	# Use $next1 until <tag>s are resolved
	my $next1 = sub {
	    if ($reader) {
		$reader->skipSiblings if $reader->depth == 2;
		my $res = $reader->nextSiblingElement;
		if (DO_ASSERTS) {
		    ($reader->name =~ m{^(?:node|way)} || $res == 0) or
			parse_warning($reader, "after skipping a node");
		}
	    }
	    no warnings 'exiting';
	    next;
	};

	# Use $next2 after <tag>s are resolved
	my $next2 = sub {
	    no warnings 'exiting';
	    next;
	};

	while(do {
	    no warnings 'uninitialized';
	    (($reader && $reader->name eq 'node') ||
	     ($dom && @nodes))
	}) {
	    my $node = $reader ? $reader : shift @nodes;

	    my $id = $node->getAttribute('id');
	    $next1->() if exists $node2ll{$id};
	    my $lat = $node->getAttribute('lat');
	    my $lon = $node->getAttribute('lon');
	    my $ll = $node2ll{$id} = $conv->($lon,$lat);

	    my %tag;
	    tie %tag, 'Tie::IxHash' if SORTED_TAGS;
	    if ($reader) {
		$reader->nextElement == 1 or last;
		while($reader->name eq 'tag') {
		    $tag{$reader->getAttribute('k')} = $reader->getAttribute('v');
		    if ($reader->nextElement != 1) {
			# no assertion needed here, we're at the end of the file
			last;
		    }
		}
	    } else {
		for my $tag ($node->findnodes('./tag')) {
		    $tag{$tag->getAttribute('k')} = $tag->getAttribute('v');
		}
	    }

	    my @or;

	    my $name   = $tag{'name'} || '';
	    my $height = exists $tag{'height'} ? $tag{'height'} : $tag{'ele'};

	    if (!defined $height) {
		if ($use_height_db) {
		    my $lon_pad = padding($lon);
		    my $lat_pad = padding($lat);
		    my $lon_lat_pad = "$lon_pad,$lat_pad";
		    if (exists $height_db{$lon_lat_pad} ) {
			$height = $height_db{$lon_lat_pad};
		    }
		}
            } else {
		# ignore metric description: "2 m" => "2"
	        $height =~ s/\s*m$//;

		# still some non-integers, extract number
		if ($height !~ /^[\d\.]+$/) {
		    $height = ($height =~ /([\d\.]+)/ ? $1 : 0);
	        }
		$height = int($height + 0.5);
	    }
	    warn "height: $height, $name\n" if defined $height && $height ne '' && $debug >= 2;

	    if (defined $parsefor_key) {
		my $found_val = $tag{$parsefor_key};
		if (defined $found_val && $found_val =~ $parsefor_val) {
		    push @or, ["-", $found_val];
		}
	    } elsif (exists $tag{'railway'}) {
		my $railway_cat = $tag{'railway'};
		if ($railway_cat =~ m{^(?:station|halt)$}) {
		    if ($name) {
			# fix name, remove "Bahnhof" if at beginning (XXX do it language independent?):
			$name =~ s{^(?:Bahnhof|Bhf\.?)\s+(.+)}{$1};
		    }

		    if (exists $tag{'station'} && $tag{'station'} eq 'light_rail') {
			push @or, ['sbahnhof', 'SA', name => normalize_name_sbahnhof($name)];
		    } elsif (exists $tag{'railway'} && $tag{'railway'} eq 'station' && 
			     exists $tag{'hvv:psv_type'} && $tag{'hvv:psv_type'} eq 'S'
		    ) { # Hamburg
			push @or, ['sbahnhof', 'SA', name => normalize_name_sbahnhof($name)];
		    } elsif (exists $tag{'station'} && $tag{'station'} eq 'subway') {
			push @or, ['ubahnhof', 'UA', name => normalize_name_ubahnhof($name)];
		    } else {
			# We don't know which layer this station belongs to,
			# so resolve it later.
			# XXX Maybe this should cause a warning? But currently "normal" railway stations
			# do not have a "station" tag, just light_rail and subway
			resolve_node_later("railway", $name, $ll);
			$next2->();
		    }
		} elsif ($railway_cat eq 'level_crossing') {
		    push @or, ["ampeln", "B"];
		} elsif ($railway_cat eq 'crossing') {
		    # railway=level_crossing is for roads, and
		    # railway=crossing for pedestrian crossings,
		    # mainly on tramways, which should not be rendered
		    # at all
		    $next2->();
		} elsif ($railway_cat eq 'tram_stop') { # takes precedence over highway=bus_stop
		    add_underline(\@or, '_oepnv', 'Tramstop');
		} else {
		    $unhandled{"node-railway=$railway_cat"}++;
		}
	    } elsif (exists $tag{'highway'}) {
		my $highway_cat = $tag{'highway'};
		if ($highway_cat eq 'traffic_signals') {
		    push @or, ["ampeln", "X"];
		} elsif ($highway_cat eq 'bus_stop') {
		    add_underline(\@or, "_oepnv", "Busstop");
		} elsif ($highway_cat eq 'crossing' &&
			 ((exists $tag{crossing} && ($tag{crossing} eq 'uncontrolled' || $tag{crossing} eq 'zebra')) ||
			  (exists $tag{crossing_ref} && $tag{crossing_ref} eq 'zebra'))
			) {
		    push @or, ['zebrastreifen', 'Zs'];
		} elsif ($highway_cat eq 'crossing' &&
			 exists $tag{'crossing'} && $tag{'crossing'} eq 'traffic_signals') {
		    push @or, ['ampeln', 'F'];
		} else {
		    $unhandled{"node-highway=$highway_cat"}++;
		}
	    } elsif (exists $tag{'barrier'} && $tag{'barrier'} eq 'cycle_barrier') {
		$name = $lang eq 'en' ? 'Cycle barrier' : 'Drngelgitter' if !$name;
		push @or, ['gesperrt', 'BNP:5', name => $name];
	    } elsif (exists $tag{'barrier'} && $tag{'barrier'} =~ m{^(gate|swing_gate)$}) {
		# XXX probably should check for tags like bicycle=yes, and if missing,
		# XXX turn this in a non-passable blocking (maybe using something like
		# XXX ... \t 3 * $coord * ?
		$name = $lang eq 'en' ? 'Gate' : 'Tor' if !$name;
		push @or, ['gesperrt', 'BNP:10', name => $name];
	    } elsif (exists $tag{'place'}) {
	    HANDLED: {
		    my $place = $tag{'place'};
		    my $population = exists $tag{'population'} ? $tag{'population'} : $tag{'openGeoDB:population'};

		    if ($population &&
			$place =~ m{^(
					city
				    |   town
				    |   village
				    )$}x
		       ) {
			my $cat;
			if ($population >= 200000) {
			    $cat = 6;
			} elsif ($population >= 50000) {
			    $cat = 5;
			} elsif ($population >= 20000) {
			    $cat = 4;
			} elsif ($population >= 5000) {
			    $cat = 3;
			} elsif ($population >= 2000) {
			    $cat = 2;
			} else {
			    $cat = 1;
			}
			push @or, ["orte", $cat];
			$place_to_info{$ll} = {
					       population => $population,
					       (defined $tag{'is_in:country_code'} ? ('is_in:country_code' => $tag{'is_in:country_code'}) : ()),
					      };
			last HANDLED;
		    }

		    if ($place =~m{^(  allotments
				   |   farm
				   |   hamlet
				   |   isolated_dwelling
				   |   borough
				   |   suburb
				   )$}x) {
			push @or, ["orte", 0];
		    } elsif ($place eq 'village') {
			push @or, ["orte", 1];
		    } elsif ($place eq 'town') {
			push @or, ["orte", 3];
		    } elsif ($place eq 'city') {
			push @or, ["orte", 5];
		    } elsif ($place =~ m{^(island|islet)$}) {
			# XXX an interim solution --- a better solution
			# would check for the coastline of this island and
			# create a proper F:I item
			push @or, ["wasserstrassen", "I"],
		    } elsif ($place =~ m{^(
					     neighbourhood
					 |   continent
					 |   country # administratively declared places
					 |   state
					 |   region
					 |   province
					 |   district
					 |   county
					 |   municipality
					 )$}x) {
			# ignore, but still added to _unhandled file
		    } else {
			$unhandled{"node-place=$place"}++;
		    }
		}
	    } elsif (exists $tag{'amenity'}) {
		push @or, $do_amenity->(0, $tag{'amenity'}, \%tag);
	    } elsif (exists $tag{'historic'}) {
		push @or, $do_historic->(0, $tag{'historic'}, \%tag);
	    } elsif (exists $tag{'tourism'}) {
		push @or, $do_tourism->(0, $tag{'tourism'}, \%tag);
	    } elsif (exists $tag{'aeroway'} && $tag{'aeroway'} eq 'aerodrome') {
		push @or, ['sehenswuerdigkeit', 'SW|IMG:airport'];
	    } elsif (exists $tag{'power'}) {
		add_underline(\@or, "_power", "X");
	    } elsif (exists $tag{'aerialway'} && $tag{'aerialway'} eq 'station') {
		push @or, ['rbahnhof', 'Ropeway'];
	    }

	    if (defined $height && $height =~ m{^[+-]?\d+}) {
		push @or, ["hoehe", "X", name => $height];
	    }

	    if (exists $tag{'icao'}) {
		push @or, ["icao", "X", name => $tag{icao} . (defined $name ? " ($name)" : '')];
	    }

	    if (exists $tag{'traffic_sign'} && $tag{'traffic_sign'} eq 'city_limit') {
		push @or, ['ortsschilder', 'OS'];
	    }

	    my $ignore_unhandled = $ignore_unhandled;
	    if ($do_addr->(\%tag, ll => $ll)) {
		$ignore_unhandled = 1;
	    }

	    if (!@or && !$ignore_unhandled && !$ignore_underline) {
		$do_unhandled->(\$name, \%tag);
		if ($name ne "" && !exists $tag{'obsolete_boundary'}) {
		    push @or, ["_unhandled", "X"];
		}
	    }

	    push @or, $do_fixme->(\%tag);

	    for my $or (@or) {
		my($out_file, $cat, %args) = @$or;

		my $name = $name;
		if ($args{name}) {
		    $name = $args{name};
		}

		$out{$out_file} ||= []; # XXX cannot autovivify in $set_url_directive
		$set_url_directive->(\%tag, $out{$out_file});
		push @{$out{$out_file}}, [$name, [$ll], $cat];
	    }
	}
    }
}

######################################################################
# WAYS
my %seenway;
my %way2nodes;
for my $osm_file (@osm_files) {
    if ($v) {
	if ($tp) {
	    print STDERR $tp->report("\r  ways done %p elapsed: %L, ETA %E (" . substr(basename($osm_file),0,28) . ")", $osm_file_i++);
	} else {
	    warn "Parse $osm_file for ways...\n";
	}
    }
    my($dom, $reader) = open_osm($osm_file);
    set_info_handler($osm_file, $dom, $reader);

 PARSE: {
	my @ways;
	if ($dom) {
	    @ways = $dom->findnodes('/osm/way');
	} else {
	    $reader->nextElement('way') == 1 or last PARSE;
	    if (DO_ASSERTS) {
		$reader->name eq 'way'
		    or parse_warning($reader, "searching for <way>");
	    }
	}

	my $next1 = sub {
	    if ($reader) {
		$reader->skipSiblings if $reader->depth == 2;
		my $res = $reader->nextSiblingElement;
		if (DO_ASSERTS) {
		    ($res == 0 || $reader->name =~ m{^(?:way|relation)}) or
			parse_warning($reader, "after skipping a way");
		}
	    }
	    no warnings 'exiting';
	    next;
	};

	while (do {
	    no warnings 'uninitialized';
	    (($reader && $reader->name eq 'way') ||
	     ($dom && @ways))
	}) {
	    my $way = $reader ? $reader : shift @ways;

	    my $id = $way->getAttribute('id');
	    $next1->() if exists $seenway{$id};
	    $seenway{$id} = 1;
	    my $visible = $way->getAttribute('visible');
	    $next1->() if $visible && $visible eq 'false';

	    my @or;

	    my @nodes;
	    my %tag;
	    tie %tag, 'Tie::IxHash' if SORTED_TAGS;
	    if ($dom) {
		@nodes = map { $_->textContent } $way->findnodes('./nd/@ref');
		for my $tag ($way->findnodes('./tag')) {
		    $tag{$tag->getAttribute('k')} = $tag->getAttribute('v');
		}
	    } else {
		$reader->nextElement == 1 or last;
		while(1) {
		    my $node_name = $reader->name;
		    if ($node_name eq 'nd') {
			push @nodes, $reader->getAttribute('ref');
		    } elsif ($node_name eq 'tag') {
			$tag{$reader->getAttribute('k')} = $reader->getAttribute('v');
		    } else {
			last;
		    }
		    $reader->nextElement == 1 or last;
		}
	    }

	    if (!@nodes) {
		warn "Broken <way>: no nodes found " . ($reader ? "approx. line " . $reader->lineNumber : "");
	    }

	    my $is_area = sub {
		$nodes[0] == $nodes[-1];
	    };

	    if ($handle_relations) {
		$way2nodes{$id} = \@nodes;
	    }

	    my $name   = $tag{'name'} || '';
	    my $height = exists $tag{'height'} ? $tag{'height'} : $tag{'ele'};
	    my($ref, $ref_is_consumed);
	    my @aliases_strassen;

	    if (exists $tag{'ref'}) {
		$ref = $tag{'ref'};
		if (!$name) {
		    $name = $ref;
		    $ref_is_consumed = 1;
		}
	    }

	    if ($debug >= 2) {
		$name .= "; id=$id";
	    }

	    my $place    = $tag{'place'};

	    my $do_railway = sub {
		my $railway_cat = $tag{'railway'};

		my $addcat = "";
		if (exists $tag{'tunnel'} && $tag{'tunnel'} eq 'yes') {
		    $addcat .= "::Tu";
		} elsif (exists $tag{'bridge'} && $tag{'bridge'} =~ BRIDGE_YES_RX) {
		    $addcat .= "::Br";
		}

		if      ($railway_cat eq 'subway') {
		    my $addservice = is_rail_service("U", $name, \@nodes) ? 'Betrieb' : '';
		    push @or, ["ubahn", "U".$addservice.$addcat]; # no zones
		    resolve_node_now('railway', 'ubahnhof', "U", \@nodes);
		} elsif ($railway_cat eq 'light_rail') {
		    my $addservice = is_rail_service("S", $name, \@nodes) ? 'Betrieb' : '';
		    push @or, ["sbahn", "S".$addservice.$addcat]; # no zones
		    resolve_node_now('railway', 'sbahnhof', "S", \@nodes);
		} elsif ($railway_cat eq 'rail') {
		    if ($name =~ m{^S\d+(,\s+S\d+)*$}) {
			# in Munich, S-Bahn is not marked as "light_rail"
			push @or, ["sbahn", "S".$addcat];
			resolve_node_now('railway', 'sbahnhof', "S", \@nodes);
		    } else {
			push @or, ["rbahn", "R".$addcat];
			resolve_node_now('railway', 'rbahnhof', "R", \@nodes);
		    }
		} elsif ($railway_cat =~ m{^(service|preserved)$}) {
		    # example for "preserved": Buckower Kleinbahn (Museumsbetrieb)
		    push @or, ["rbahn", "RG".$addcat];
		    resolve_node_now('railway', 'rbahnhof', "RG", \@nodes);
		} elsif ($railway_cat =~ m{^(?:abandoned|disused|razed)$}) {
		    push @or, ["rbahn", "R0".$addcat];
		    resolve_node_now('railway', 'rbahnhof', "R0", \@nodes);
		} elsif ($railway_cat =~ CONSTRUCTION_RX) { # ignoring PROJECTED_RX
		    my $construction = $tag{'construction'} || '';
		    if ($construction eq 'subway') {
			push @or, ["ubahn", "UBau".$addcat];
			resolve_node_now('railway', 'ubahnhof', "UBau", \@nodes);
		    } elsif ($construction eq 'light_rail') {
			push @or, ["sbahn", "SBau".$addcat];
			resolve_node_now('railway', 'sbahnhof', "SBau", \@nodes);
		    } else {
			push @or, ["rbahn", "RBau".$addcat];
			resolve_node_now('railway', 'rbahnhof', "RBau", \@nodes);
		    }
		} elsif ($railway_cat =~ m{^(?:narrow_gauge)$}) { # z.B. Parkeisenbahnen
		    push @or, ["rbahn", "RP".$addcat];
		    resolve_node_now('railway', 'rbahnhof', "RP", \@nodes);
		} elsif ($railway_cat =~ m{\btram\b}) {
		    push @or, ["comments_tram", "CS".$addcat];
		} else {
		    return 0; # unhandled
		}
		return 1; # handled
	    };

	    if (defined $height ) {
		#warn "hoehe\n";
		#push @or, ["hoehe", "N"];
		#push @or, ["kneipen", $F."X"];
	    }

	    # Flag for gesperrt=2 or gesperrt_car=2, may be used to
	    # override completely blocked over oneway blockings
	    my $gesperrt_car_seen;
	    my $gesperrt_seen;

	    my $quality_cat;
	    my $quality_name;
	    if (exists $tag{'surface'} || exists $tag{'smoothness'}) {
		my $surface    = $tag{'surface'}    || '';
		my $smoothness = $tag{'smoothness'} || '';

		my @quality_name;

		if ($surface ne '') {
		    if      ($surface eq 'paved') {
			$quality_cat = 'Q0';
			@quality_name = 'befestigt (betoniert, asphaltiert oder wassergebunden)';
		    } elsif ($surface eq 'asphalt') {
			$quality_cat = 'Q0';
			@quality_name = 'Asphalt';
		    } elsif ($surface eq 'concrete') {
			$quality_cat = 'Q0';
			@quality_name = 'Beton';
		    } elsif ($surface eq 'concrete_plates' || $surface eq 'concrete:plates' || $surface eq 'concrete:lanes') {
			$quality_cat = 'Q2+';
			@quality_name = 'Plattenweg';
		    } elsif ($surface eq 'cobblestone') {
			$quality_cat = 'Q2';
			@quality_name = 'Kopfsteinpflaster';
		    } elsif ($surface eq 'unhewn_cobblestone') {
			$quality_cat = 'Q3';
			@quality_name = 'schlechtes Kopfsteinpflaster';
		    } elsif ($surface eq 'cobblestone:flattened') {
			$quality_cat = 'Q2+';
			@quality_name = 'glattes Kopfsteinpflaster';
		    } elsif ($surface eq 'sett') {
			$quality_cat = 'Q2+';
			@quality_name = 'gepflastert';
		    } elsif ($surface eq 'gravel') {
			$quality_cat = 'Q2+';
			@quality_name = 'Schotter';
		    } elsif ($surface eq 'fine_gravel') {
			$quality_cat = 'Q1-';
			@quality_name = 'Feinschotter';
		    } elsif ($surface eq 'unpaved' || $surface eq 'dirt' || $surface eq 'earth' || $surface eq 'ground') {
			$quality_cat = 'Q2';
			@quality_name = 'unbefestigt';
		    } elsif ($surface eq 'sand') {
			$quality_cat = 'Q3';
			@quality_name = 'sandig';
		    } elsif ($surface eq 'paving_stones') {
			$quality_cat = 'Q1';
			@quality_name = 'Verbundsteinpflaster';
		    } elsif ($surface eq 'compacted') {
			$quality_cat = 'Q1';
			@quality_name = 'wassergebundene Decke';
		    } elsif ($surface eq 'grass') {
			$quality_cat = 'Q3';
			@quality_name = 'Gras';
		    } elsif ($surface eq 'wood') {
			$quality_cat = 'Q1';
			@quality_name = 'Holz';
		    }
		    if ($lang eq 'en') {
			@quality_name = $surface;
		    }
		}

		if ($smoothness ne '') {
		    if (defined $smoothness_to_Q{$smoothness}) {
			$quality_cat = $smoothness_to_Q{$smoothness}; # overrides before
			push @quality_name, $lang eq '' ? $smoothness_to_de{$smoothness}||$smoothness : $smoothness;
		    } else {
			push @or, ['gesperrt', '2', name => $smoothness]; $gesperrt_seen = 1;
		    }
		}

		$quality_name = join ", ", @quality_name if @quality_name;
	    }

	    my $handicap_cat;
	    my $handicap_name;

	    my $set_handicap = sub {
		# XXX missing '-' and '+' handling
		if (!defined $handicap_cat || substr($handicap_cat,1,1) < substr($_[0],1,1)) {
		    $handicap_cat = $_[0];
		    $handicap_name = $_[1];
		}
	    };

	    my $maybe_add_ref_to_name = sub {
		if (!$ref_is_consumed && defined $ref && $name ne $ref) {
		    $name .= " ($ref)";
		    $ref_is_consumed = 1;
		}
	    };

	    my $maybe_add_ref_to_aliases = sub {
		if (!$ref_is_consumed && defined $ref && $name ne $ref) {
		    push @aliases_strassen, $ref;
		    $ref_is_consumed = 1;
		}
	    };

	    my $adjust_bab_name = sub {
		$maybe_add_ref_to_name->();
		if ($country eq 'de') {
		    $name =~ s{\bA\s*(\d+)}{BAB$1};
		}
	    };

	    if (exists $tag{'highway'}) {
		my $highway_cat = $tag{'highway'};
		my $is_motorway = 0;
		my $is_motorroad = exists $tag{'motorroad'} && $tag{'motorroad'} eq 'yes' ? 1 : 0; # Kraftfahrzeugstrae
		my $bicycle_tag = $tag{'bicycle'};
		my $foot_tag = $tag{'foot'};
		if (defined $bicycle_tag && $bicycle_tag eq 'no') {
		    if (defined $foot_tag && $foot_tag eq 'no') {
			push @or, ["gesperrt", 2]; $gesperrt_seen = 1;
		    } else {
			# Heuristic: if cyclists are explicit
			# disallowed but pedestrians not, then assume
			# that dismounting is possible. Does not work
			# everywhere, example: Heerstrasse (foot=no is
			# lacking here)
			$set_handicap->('q4', $cyclists_dismount_label);
		    }
		}
		if (defined $bicycle_tag && $bicycle_tag eq 'dismount') {
		    $set_handicap->('q4', $cyclists_dismount_label);
		}

		if ($experiments{add_postal_code} && exists $tag{'postal_code'}) {
		    $name .= " ($tag{'postal_code'})";
		}

		my $addcat = "";
		if (exists $tag{'tunnel'}) {
		    if ($tag{'tunnel'} eq 'yes') {
			$addcat .= "::Tu";
		    } elsif ($tag{'tunnel'} eq 'building_passage' && $highway_cat !~ m{^(service|footway|path)$}) { # limit to "real" highways
			$addcat .= "::Tu";
		    }
		} elsif (exists $tag{'bridge'} && $tag{'bridge'} =~ BRIDGE_YES_RX) {
		    $addcat .= "::Br";
		}

		if      ($highway_cat =~ HIGHWAY_BAB_RX || $is_motorroad) {
		    $adjust_bab_name->();
		    push @or, ["strassen_bab", "BAB".$addcat];
		    $is_motorway = 1;
		} elsif ($highway_cat =~ HIGHWAY_TRUNK_RX) {
		    # It's possible that trunk highways are allowed for cyclists
		    # (e.g. Pfaffendorfer Brcke in Koblenz). So only mark them
		    # as motorways if $is_motorroad is true (already handled above)
		    # or if it's explicitly forbidden for bicycles.
		    if (defined $bicycle_tag && $bicycle_tag eq 'no') {
			$adjust_bab_name->();
			push @or, ["strassen_bab", "BAB".$addcat];
			$is_motorway = 1;
		    } else {
			push @or, ["strassen", "HH".$addcat];
		    }
		} elsif ($highway_cat =~ HIGHWAY_HH_RX) {
		    push @or, ["strassen", "HH".$addcat];
		} elsif ($highway_cat =~ HIGHWAY_H_RX) {
		    push @or, ["strassen", "H".$addcat];
		} elsif ($highway_cat =~ HIGHWAY_NH_RX) {
		    push @or, ["strassen", "NH".$addcat];
		} elsif ($highway_cat =~ HIGHWAY_N_RX) {
		    push @or, ["strassen", "N".$addcat];
		} elsif ($highway_cat eq 'road') {
		    push @or, ['strassen', "N".$addcat];
		    push @or, ['fragezeichen', '?', name => 'road category missing'];
		} elsif ($highway_cat eq 'cycleway') {
		    push @or, ["strassen", "NN".$addcat];
		} elsif ($highway_cat eq 'unsurfaced') { # XXX not mentioned in Map_Features, maybe fix data and remove?
		    push @or, ["strassen", "N".$addcat];
		    if (!defined $quality_cat) {
			$quality_cat = "Q2";
			$quality_name = $lang eq 'en' ? $highway_cat : "unbefestigte Strae";
		    }
		} elsif ($highway_cat eq 'track') {
		    push @or, ["strassen", "N".$addcat];
		    my $add_quality_name;
		    if ($tag{tracktype}) { # may override $quality_cat
			my @all_grades = grep { m{^grade\d+$} } split /\s*;\s*/, $tag{tracktype};
			my @num_grades = map { m{grade(\d+)}; $1 } @all_grades;
			if (@num_grades) {
			    my $grade = max @num_grades;
			    if (defined(my $q = $grade_to_Q{$grade})) {
				$quality_cat = $q;
			    }
			    if (defined $quality_name) {
				$quality_name .= " (" . join(", ", @all_grades) . ")";
			    } else {
				$add_quality_name = " (" . join(", ", @all_grades) . ")";
			    }
			}
		    }
		    if (!defined $quality_cat) {
			$quality_cat = "Q2";
		    }
		    if (!defined $quality_name) {
			$quality_name = $lang eq 'en' ? $highway_cat : "Fahrweg";
		    }
		    if ($add_quality_name) {
			$quality_name .= $add_quality_name;
		    }
		} elsif ($highway_cat eq 'path') {
		    push @or, ["strassen", "NN".$addcat];
		    if (!$tag{bicycle} || $tag{bicycle} eq 'no') {
			if (!defined $quality_cat) {
			    $quality_cat = "Q3";
			    $quality_name = $lang eq 'en' ? $highway_cat : "Pfad";
			}
		    }
		} elsif ($highway_cat =~ m{^(footway|pedestrian)$}) {
		    my $label = ($highway_cat eq 'footway'
				 ? ($lang eq 'en' ? 'footway' : 'Fuweg')
				 : ($lang eq 'en' ? 'pedestrian area' : 'Fugngerzone')
				);
		    push @or, ["strassen", "NN".$addcat];
		    if (defined $bicycle_tag) {
			if ($bicycle_tag =~ m{^(yes|designated|official)$}) {
			    $set_handicap->('q1', $label . ($lang eq 'en' ? ', cycling allowed' : ', Radfahren erlaubt'));
			} elsif ($bicycle_tag eq 'destination') {
			    $set_handicap->('q1', $label . ($lang eq 'en' ? ', cycling to destination allowed' : ', Radfahren fr Anlieger erlaubt'));
			} elsif ($bicycle_tag =~ m{^(permissive|tolerated)$}) { # XXX however, "tolerated" is not documented in https://wiki.openstreetmap.org/wiki/Bicycle --- maybe should be fixed in data?
			    $set_handicap->('q2', $label . ($lang eq 'en' ? ', cycling permissive' : ', Radfahren geduldet'));
			} elsif ($bicycle_tag eq 'no') {
			    $set_handicap->('q4', $label . ($lang eq 'en' ? ', cycling not allowed' : ', Radfahren nicht erlaubt, ggfs. schieben'));
			} elsif ($bicycle_tag eq 'dismount') {
			    # Already handled above
			} elsif ($bicycle_tag eq 'private' && $gesperrt_seen) {
			    # probably already handled if there was an access tag before
			} else {
			    warn "Found unhandled bicycle=$bicycle_tag together with $highway_cat in way '$id'\n";
			}
		    } else {
			my $cat = $highway_cat eq 'footway' ? 'q3' : 'q4'; # unklare Situation fr Radfahrer
			$set_handicap->($cat, $label);
		    }
		} elsif ($highway_cat eq 'living_street') {
		    push @or, ["strassen", "N".$addcat];
		    push @or, ["radwege_exact", "RW6;RW6", name => ($name ? "$name: " : "") . ($lang eq 'en' ? 'living street' : 'verkehrsberuhigter Bereich')];
		} elsif ($highway_cat =~ CONSTRUCTION_RX) {
		    my $construction = $tag{'construction'} || ""; # what will the type of street be?
		    if      ($construction =~ HIGHWAY_BAB_RX) {
			$adjust_bab_name->();
			push @or, ["strassen_bab", "BAB::inwork"];
		    } elsif ($construction =~ HIGHWAY_HH_RX) {
			push @or, ['strassen', 'HH'];
			push @or, ['gesperrt', '2:inwork']; $gesperrt_seen = 1;
		    } elsif ($construction =~ HIGHWAY_H_RX) {
			push @or, ['strassen', 'H'];
			push @or, ['gesperrt', '2:inwork']; $gesperrt_seen = 1;
		    } elsif ($construction =~ HIGHWAY_NH_RX) {
			push @or, ['strassen', 'NH'];
			push @or, ['gesperrt', '2:inwork']; $gesperrt_seen = 1;
		    } elsif ($construction =~ HIGHWAY_N_RX) {
			push @or, ['strassen', 'N'];
			push @or, ['gesperrt', '2:inwork']; $gesperrt_seen = 1;
		    } else {
			push @or, ['strassen', 'N'];
			push @or, ['gesperrt', '2:inwork']; $gesperrt_seen = 1;
		    }
		} elsif ($highway_cat =~ PROJECTED_RX) {
		    push @or, ['fragezeichen', '?::projected'];
		} elsif ($highway_cat eq 'steps') {
		    push @or, ["strassen", "NN".$addcat];
		    my $name       = $lang eq 'en' ? 'steps' : 'Treppe';
		    my $lost_time  = 30; # an average if a step_count is missing
		    my $step_count = $tag{'step_count'};
		    if (defined $step_count) {
			if ($lang eq 'en') {
			    $name = $step_count . ' step' . ($step_count == 1 ? '' : 's');
			} else {
			    $name .= " ($step_count Stufe" . ($step_count == 1 ? '' : 'n');
			}
			$lost_time = int($step_count * 0.7); # XXX need more scientific background for the coefficient
		    }
		    push @or, ["gesperrt", "0:$lost_time", name => $name]; # XXX really should be only one point, or maybe better, change bbbike to handle line features for "Treppe"; Also missing: precalculated angle. In case of a line feature I need to think something else...
		} elsif ($highway_cat eq 'cycleroad') { # It seems that this tag is not anymore in use, see bicycle_road=yes
		    push @or, ["strassen", "N".$addcat];
		    push @or, ["radwege_exact", "RW7;RW7"];
		} else {
		    $unhandled{"highway=$highway_cat"}++;
		}

		$maybe_add_ref_to_aliases->();

		my $access = $tag{'access'};
		if ($access && $access =~ m{^(?:no|private)$}) {
		    my $bicycle = $tag{'bicycle'};
		    if ($bicycle && $bicycle =~ m{^(yes|designated|official|permissive|tolerated)$}) {
			if (!$gesperrt_car_seen) {
			    push @or, ["gesperrt_car", 2]; $gesperrt_car_seen = 1;
			}
		    } else {
			if (!$gesperrt_seen) {
			    push @or, ["gesperrt", 2]; $gesperrt_seen = 1;
			}
		    }
		}

		if (!$gesperrt_seen) {
		    my $sac_scale = $tag{'sac_scale'};
		    if ($sac_scale && $sac_scale ne 'hiking') {
			push @or, ["gesperrt", '2', name => "sac_scale=$sac_scale"];
		    } else {
			my $path = $tag{'path'};
			if ($path && $path eq 'climbing_access') {
			    push @or, ["gesperrt", '2', name => "climbing_access"];
			}
		    }
		}

		my $oneway = $tag{'oneway'};
		my $no_oneway_for_cyclists;
		if ($oneway && $highway_cat !~ PROJECTED_RX) {
		    my $reversed = 1;
		    # Special case -1, see
		    # http://wiki.openstreetmap.org/index.php/Key:oneway
		    if ($oneway =~ m{^(-1|reverse)$}) {
			$oneway = "yes";
			$reversed = 0;
		    } elsif ($oneway =~ m{^(true|1)$}) {
			$oneway = "yes";
		    }
		    if ($oneway eq 'yes') {
			$no_oneway_for_cyclists =
			    (
			     (exists $tag{'oneway:bicycle'} && $tag{'oneway:bicycle'} eq 'no') || # documented in http://wiki.openstreetmap.org/wiki/Key:oneway
			     (exists $tag{'bicycle'} && $tag{'bicycle'} eq 'opposite') || # seen in Wrzburg, uere Pleich, but no documentation/discussion for it
			     (exists $tag{'cycleway'} && $tag{'cycleway'} =~ m{^opposite(?:$|_lane|_track)$}) # described in http://wiki.openstreetmap.org/wiki/DE:Germany_roads_tagging
			    );
			if ($is_motorway || $no_oneway_for_cyclists) {
			    if (!$gesperrt_car_seen) {
				push @or, ['gesperrt_car', '1', reversed => $reversed];
			    }
			} else {
			    if (!$gesperrt_seen) {
				if ($highway_cat eq 'cycleway') {
				    # usually this generates to much clutter in
				    # the map ("straenbegleitende Radwege"), so
				    # ignore map display
				    push @or, ["gesperrt", "1::igndisp", reversed => $reversed];
				} else {
				    push @or, ["gesperrt", "1", reversed => $reversed];
				}
			    }
			}
		    } else {
			$unhandled{"oneway=$oneway"}++;
		    }
		}

		if (defined $quality_cat) {
		    push @or,
			["qualitaet_s",
			 $quality_cat,
			 name => ($name ? $name : '') . ($name && $quality_name ? ': ' : '') . ($quality_name ? $quality_name : ''),
			];
		}

		if (defined $handicap_cat) {
		    push @or,
			["handicap_s",
			 $handicap_cat,
			 name => ($name ? $name : '') . ($name && $handicap_name ? ': ' : '') . ($handicap_name ? $handicap_name : ''),
			];
		}

		if (exists $tag{lit} && $tag{lit} =~ m{^(?:no|broken|disused)$}) {
		    push @or,
			['nolighting', 'NL', name => ''];
		}

		my $rcn_ref = $tag{'rcn_ref'};
		my $lcn_ref = $tag{'lcn_ref'};
		if ($rcn_ref) {
		    push @or, ["comments_route", "radroute", name => $rcn_ref];
		}
		if ($lcn_ref) {
		    push @or, ["comments_route", "radroute", name => $lcn_ref];
		}

		if (exists $tag{'railway'}) {
		    $do_railway->(); # typically tram on street
		}

		my $bicycle_road = $tag{'bicycle_road'};
		if ($bicycle_road && $bicycle_road eq 'yes') {
		    push @or, ["radwege_exact", "RW7;RW7"];
		} else {
		    my $cycleway       = $tag{'cycleway'} || $tag{'cycleway:both'} || '';
		    my $cycleway_right = $tag{'cycleway:right'};
		    my $cycleway_left  = $tag{'cycleway:left'};
		    if ($cycleway || $cycleway_right || $cycleway_left) {
			# right -> 0
			# left -> 1
			my @cat = ('', '');
			my @cycleway;
			my @cycleway_bicycle; # for cycleway:$side:bicycle tag
			if ($cycleway && !$cycleway_right && !$cycleway_left) {
			    # oneway handling for legacy stuff (without cycleway:{right,left})
			    if ($oneway && $oneway eq 'yes' && !$no_oneway_for_cyclists) {
				$cycleway[0] = $cycleway;
			    } else {
				$cycleway[0] = $cycleway[1] = $cycleway;
			    }
			} else {
			    $cycleway[0] = $cycleway_right || $cycleway;
			    $cycleway[1] = $cycleway_left  || $cycleway;
			    $cycleway_bicycle[0] = $tag{'cycleway:right:bicycle'};
			    $cycleway_bicycle[1] = $tag{'cycleway:left:bicycle'};
			}
			for my $inx (0, 1) {
			    my $cycleway = $cycleway[$inx];
			    next if !$cycleway;

			    if      ($cycleway eq 'lane') {
				$cat[$inx] = 'RW4';
			    } elsif ($cycleway eq 'track') {
				my $cycleway_bicycle = $cycleway_bicycle[$inx];
				if ($cycleway_bicycle && $cycleway_bicycle eq 'designated') {
				    $cat[$inx] = 'RW2';
				} else { # z.B. 'yes'
				    $cat[$inx] = 'RW1';
				}
			    } elsif ($cycleway eq 'opposite_track' ||
				     $cycleway eq 'opposite_lane' # sollte in Berlin nicht vorkommen
				    ) {
				$cat[$inx] = 'RW8';
			    } elsif ($cycleway eq 'opposite') {
				# wird in Zusammenhang mit oneway verwendet
			    } elsif ($cycleway eq 'share_busway') {
				$cat[$inx] = 'RW5';
			    }
			}
			if ($cat[0] || $cat[1]) {
			    push @or, ["radwege_exact", join(';', @cat)];
			} else {
			    $unhandled{'cycleway-'.($cycleway[0]||'').'-'.($cycleway[1]||'')}++;
			}
		    }
		}

		if (exists $tag{'incline'} && $highway_cat ne 'steps') {
		    my $incline = $tag{'incline'};
		    if ($incline eq 'up') {
			push @or, ['comments_mount', 'St;', name => $incline_label];
		    } elsif ($incline eq 'down') {
			push @or, ['comments_mount', 'St;', name => $incline_label, reversed => 1];
		    } elsif ($incline =~ m{^([-\+])?([\d.]+)%$}) {
			my $reversed = ($1||'') eq '-';
			my $amount = $2;
			if ($amount > 0) {
			    push @or, ['comments_mount', 'St;', name => $incline_label.' '.$amount.'%', reversed => $reversed];
			}
		    } else {
			if ($debug) {
			    warn "Cannot handle incline value '$incline' in way '$id'\n";
			}
		    }
		}

	    } elsif (exists $tag{'waterway'}) {
		my $waterway_cat = $tag{'waterway'};
		if      ($waterway_cat eq 'riverbank') {
		    # Plot as an area only if it's closed
		    if ($nodes[0] eq $nodes[-1]) {
			push @or, ["wasserstrassen", "F:W"];
		    } else {
			push @or, ["wasserstrassen", "W"]; # XXX This should be fixed!
			warn_once("Found unclosed riverbank, sorry cannot use it, fallback to line plotting ...\n");
		    }
		} elsif ($waterway_cat eq 'canal') {
		    push @or, ["wasserstrassen", "W"];
		} elsif ($waterway_cat eq 'river') {
		    push @or, ["wasserstrassen", "W1"];
		} elsif ($waterway_cat eq 'stream') {
		    push @or, ["wasserstrassen", "W0"];
		} else {
		    $unhandled{"waterway=$waterway_cat"}++;
		}
	    } elsif (exists $tag{'railway'}) {
		if (!$do_railway->()) {
		    my $railway_cat = $tag{'railway'};
		    $unhandled{"railway=$railway_cat"}++;
		}
	    } elsif (exists $tag{'natural'}) {
		my $natural = $tag{'natural'};
		if      ($natural eq 'water') {
		    push @or, ["wasserstrassen", "F:W"];
		} elsif ($natural eq 'wood') {
		    push @or, ["flaechen", "F:Forest"];
		} elsif ($natural eq 'land') {
		    my $island = $tag{'island'}; # seen in Zuerich
		    my $layer  = $tag{'layer'}; # seen in Berlin
		    if (($place && $place eq 'island') ||
			($island && $island =~ m{^(?:yes|true)$}) ||
			(defined $layer && $layer ne "" && $layer == 1)
		       ) {
			push @or, ["wasserstrassen", "F:I"];
		    }
		} elsif ($natural eq 'coastline') {
		    if ($experiments{coastline_hack}) {
			push @or, ["_coastline", "W"];
			warn_once("Moving natural=coastline into _coastline file, for proceeding with the coastline hack...\n");
		    } else {
			push @or, ["wasserstrassen", "W"]; # XXX This should be fixed!
			warn_once("Found natural=coastline, sorry cannot use it, fallback to line plotting...\n");
		    }
		} else {
		    $unhandled{"natural=$natural"}++;
		}
	    } elsif (exists $tag{'boundary'}) { # Sometimes there's boundary=administrative and natural=coastline at the same time. Currently I prefer coastline over boundary (see the Usedom data). But maybe I should render both? Think of it! XXX
		my $boundary = $tag{'boundary'};
		my $admin_level = $tag{'admin_level'};
		if      ($boundary eq 'administrative') {
		    # The new-style berlin/germany-independent borders
		    push @or, ["borders", "Z" . (defined $admin_level ? $admin_level : '?')];
		    # For bbbike compat only:
		    if (defined $admin_level) {
			if (   $admin_level == 9  # (neue) Bezirke
			       || $admin_level == 10 # Ortsteile
			   ) {
			    push @or, ["berlin_ortsteile", "Z"];
			} elsif ($admin_level == 2) {
			    push @or, ["deutschland", "Z"];
			} else {
			    $unhandled{"boundary=administrative,admin_level=$admin_level"}++;
			}
		    }
		} else {
		    $unhandled{"boundary=$boundary"}++;
		}
	    } elsif (exists $tag{'sport'}) {
		my $sport = $tag{'sport'};
		if      ($sport =~ m{^(?:soccer|athletics)$}) {
		    push @or, ["flaechen", "F:Sport"];
		} else {
		    $unhandled{"sport=$sport"}++;
		}
	    } elsif (exists $tag{'leisure'}) {
		my $leisure = $tag{'leisure'};
		if      ($leisure =~ m{^(?:park|garden|recreation_ground)$}) {
		    push @or, ["flaechen", "F:P"];
		} elsif ($leisure eq 'stadium') {
		    push @or, ["flaechen", "F:Sport"];
		} elsif ($leisure eq 'cemetery') { # see below: landuse=cemetery
		    push @or, ["flaechen", $cemetery_tag->(\%tag)];
		} else {
		    $unhandled{"leisure=$leisure"}++;
		}
	    } elsif (exists $tag{'landuse'}) {
		my $landuse = $tag{'landuse'};
		if      ($landuse eq 'cemetery') {
		    push @or, ["flaechen", $cemetery_tag->(\%tag)];
		} elsif ($landuse eq 'industrial') {
		    push @or, ["flaechen", "F:Industrial"];
		} elsif ($landuse eq 'railway') {
		    push @or, ["flaechen", "F:Industrial"]; # XXX but maybe use Railway some day?
		} elsif ($landuse eq 'commercial') {
		    push @or, ["flaechen", "F:Industrial"]; # XXX but maybe use Commercial some day?
		} elsif ($landuse eq 'retail') {
		    push @or, ["flaechen", "F:Industrial"]; # XXX but maybe use Retail some day?
		} elsif ($landuse eq 'allotments') {
		    push @or, ["flaechen", "F:Orchard"];
		} elsif ($landuse eq 'forest') { # see above: natural=wood
		    push @or, ["flaechen", "F:Forest"];
		} elsif ($landuse =~ m{^(?:grass|village_green)$}) {
		    push @or, ["flaechen", "F:Green"]; # XXX not definitely clear
		    #XXX ja? nein? } elsif ($landuse eq 'water') { push @or, ["wasserstrassen", "F:W"];
		} elsif ($landuse eq 'residential') {
		    add_underline(\@or, "_building", "F:#d0d0d0");
		} else {
		    # e.g. greenfield (= Bauerwartungsland)
		    $unhandled{"landuse=$landuse"}++;
		}
	    } elsif ($place) {
		if      ($place eq 'airport') {
		    push @or, ["flaechen", "F:Ae"];
		} else {
		    $unhandled{"place=$place"}++;
		}
	    } elsif (exists $tag{'route'}) {
		my $route = $tag{'route'};
		if      ($route eq 'ferry') {
		TRY_FERRY: {
			my @comments;
			my $day_on  = $tag{'day on'};
			my $day_off = $tag{'day off'};
			my $bicycle = $tag{'bicycle'};
			if ($day_on && $day_off) {
			    push @comments, "$day_on - $day_off";
			}
			if ($bicycle && $bicycle !~ m{^(?:yes|true)$}) {
			    if ($bicycle =~ m{^(?:no|false)$}) {
				last TRY_FERRY;
			    }
			    push @comments, "bicycle=$bicycle";
			}
			my $comments = join("; ", @comments);
			push @or, ["faehren", "Q"];
			if ($comments) {
			    push @or, ["comments_ferry", "CS", name => $comments];
			}
		    }
		} else {
		    $unhandled{"route=$route"}++;
		}
	    } elsif (exists $tag{'amenity'}) {
		push @or, $do_amenity->($is_area->(), $tag{'amenity'}, \%tag);
	    } elsif (exists $tag{'historic'}) {
		push @or, $do_historic->($is_area->(), $tag{'historic'}, \%tag);
	    } elsif (exists $tag{'tourism'}) {
		push @or, $do_tourism->($is_area->(), $tag{'tourism'}, \%tag);
	    } elsif (exists $tag{'building'}) {
		my $building = $tag{'building'};
		if ($building =~ m{^(?:yes|true)$}) {
		    add_underline(\@or, "_building", "F:#cccccc");
		}
	    } elsif (exists $tag{'power'}) {
		if (exists $tag{'operator'}) {
		    $name = ($name ? $name.": " : "") . $tag{'operator'};
		}
		add_underline(\@or, "_power", "X");
	    } elsif (exists $tag{'aerialway'} && $tag{'aerialway'} =~ m{^(cable_car|gondola|chair_lift|mixed_lift)$}) { # currently only handling non-ski lift types
		push @or, ['rbahn', 'Ropeway'];
	    }

	    my $ignore_unhandled = $ignore_unhandled;
	    if ($do_addr->(\%tag, nodes => \@nodes)) {
		$ignore_unhandled = 1;
	    }

	    if (!@or && !$ignore_unhandled && !$ignore_underline) {
		$do_unhandled->(\$name, \%tag);
		if ($name ne "") {
		    my $cat;
		    if (exists $tag{'landuse'} &&
			$tag{'landuse'} =~ m{^(farm|farmland)$}
		       ) {
			$cat = 'F:#ffdead';
		    } else {
			$cat = 'X';
		    }
		    push @or, ["_unhandled", $cat];
		}
	    }

	    push @or, $do_fixme->(\%tag);

	    for my $or (@or) {
		my($out_file, $cat, %args) = @$or;

		my $name = $name;
		my @nodes = @nodes;
		if ($args{reversed}) {
		    @nodes = reverse @nodes;
		}
		if (defined $args{name}) {
		    $name = $args{name};
		}

		$out{$out_file} ||= []; # XXX cannot autovivify in $set_url_directive
		$set_url_directive->(\%tag, $out{$out_file});

		# local, official, historical, regional names etc:  
		# http://wiki.openstreetmap.org/wiki/Key:name
	      	foreach my $key (qw/int_name nat_name reg_name loc_name old_name alt_name official_name/) {
		    if (exists $tag{$key}) {
		    	push @{$out{$out_file}}, ["#: $key: $tag{$key}"];
		    }
		}

		# check_date, opening_date
		if (exists $tag{check_date}) {
		    push @{$out{$out_file}}, ["#: last_checked: $tag{check_date}"];
		}
		if (exists $tag{opening_date} && $tag{opening_date} gt $today_date) {
		    push @{$out{$out_file}}, ["#: next_check: $tag{opening_date}"];
		}

		if ($out_file eq 'strassen') {
		    for my $alias (@aliases_strassen) {
			push @{$out{$out_file}}, ["#: alias: $alias"];
		    }
		}

		if ((exists $tag{'width'} || exists $tag{'est_width'}) && $out_file eq 'strassen') {
		    my $w = $tag{'width'} || $tag{'est_width'};
		    if ($w =~ m{^([\d\.]+)(?:$|\s*m)}) {
			push @{$out{$out_file}}, ["#: carriageway_width: ${w}m"];
		    } else {
			if ($debug) {
			    if ($w =~ m{^(?:narrow)}) {
				# silently ignore
			    } else {
				warn "Found width/est_width tag, but cannot parse the value '$w' in way '$id'\n";
			    }
			}
		    }
		}
		push @{$out{$out_file}}, [$name, [map { $node2ll{$_} } @nodes], $cat];
	    }
	}
    }
}

output_addr();

######################################################################
# RELATIONS
if ($handle_relations) {
    my %seenrelation;
    for my $osm_file (@osm_files) {
	if ($v) {
	    if ($tp) {
		print STDERR $tp->report("\r  relations done %p elapsed: %L, ETA %E (" . substr(basename($osm_file),0,28) . ")", $osm_file_i++);
	    } else {
		warn "Parse $osm_file for relations...\n";
	    }
	}
	my($dom, $reader) = open_osm($osm_file);
	if ($dom) { die "NYI: no relation support with XML::LibXML, use XML::LibXML::Reader" }
	set_info_handler($osm_file, $dom, $reader);

    PARSE: {
	    $reader->nextElement('relation') == 1 or last PARSE;
	    if (DO_ASSERTS) {
		$reader->name eq 'relation'
		    or parse_warning($reader, "searching for <relation>");
	    }

	    my $next1 = sub {
		if ($reader) {
		    $reader->skipSiblings if $reader->depth == 2;
		    my $res = $reader->nextSiblingElement;
		    if (DO_ASSERTS) {
			($res == 0 || $reader->name =~ m{^(?:relation)}) or
			    parse_warning($reader, "after skipping a relation");
		    }
		}
		no warnings 'exiting';
		next;
	    };

	    while(do {
		no warnings 'uninitialized';
		($reader && $reader->name eq 'relation');
	    }) {
		my $relation = $reader;
		my $id = $relation->getAttribute('id');
		$next1->() if exists $seenrelation{$id};
		$seenrelation{$id} = 1;

		my @members;
		my %tag;
		tie %tag, 'Tie::IxHash' if SORTED_TAGS;
		$reader->nextElement == 1 or last;
		while(1) {
		    my $node_name = $reader->name;
		    if ($node_name eq 'member') {
			push @members, { type => $reader->getAttribute('type'),
					 ref  => $reader->getAttribute('ref'),
					 role => $reader->getAttribute('role'),
				       };
		    } elsif ($node_name eq 'tag') {
			$tag{$reader->getAttribute('k')} = $reader->getAttribute('v');
		    } else {
			last;
		    }
		    $reader->nextElement == 1 or last;
		}

		## In the Berlin data there's only one relation tagged
		## with visible=no (why not false?), so probably wrong
		## and does not need to be handled
		#$next1->() if $tag{'visible'} && $tag{'visible'} eq 'false';

		my @or;
		my $name   = $tag{'name'} || '';

		if (my $route = $tag{'route'}) {
		    if ($route eq 'bicycle') {
			for my $member (@members) {
			    my $role = $member->{role} || '';
			    my $cat = ($role eq 'forward' ? 'radroute;' :
				       $role eq 'backward' ? ';radroute' : 'radroute');
			    push @or, ['comments_route', $cat, $member];
			}
		    } elsif ($route eq 'bus') {
			for my $member (@members) {
			    my $role = $member->{role} || '';
			    my $cat = ($role eq 'forward' ? 'Bus;' :
				       $role eq 'backward' ? ';Bus' : 'Bus');
			    push @or, ['_oepnv', $cat, $member];
			}
		    }
		} elsif ($tag{boundary} && $tag{boundary} eq 'administrative') {
		    my $admin_level = $tag{admin_level} || '?';
		    for my $member (@members) {
			push @or, ['_boundary', 'Z'.$admin_level, $member];
		    }
		} else {
		    my $type = $tag{type};
		    if (!defined $type) {
			# XXX https://wiki.openstreetmap.org/wiki/DE:Relationen
			# suggests that every relation has a type. However this suggestion
			# is missing in the English variant and on
			# https://wiki.openstreetmap.org/wiki/Types_of_relation
			warn "relation without type in relation $id\n";
			$type = "";
		    }
		    if ($type eq 'multipolygon' &&
			(
			 ($tag{waterway} && $tag{waterway} eq 'riverbank') ||
			 ($tag{natural} && $tag{natural} eq 'water')
			)
		       ) {
			my $new_members = process_multipolygon(\@members, $id);
			my @islands_or;
			my @waters_or;
			for my $member (@$new_members) {
			    my $role = $member->{role} || '';
			    if ($role eq 'inner') {
				push @islands_or, ['wasserstrassen', 'F:I', $member, name => '']; # explicitely reset name (it's valid for the water only)
			    } elsif ($role eq 'outer') {
				push @waters_or, ['wasserstrassen', 'F:W', $member];
			    }
			}
			# order is important here!
			push @or, @waters_or, @islands_or;
		    } elsif ($type eq 'multipolygon' && ($tag{landuse}||'') eq 'forest') {
			# XXX A very rough solution: ignore the "holes" (inner members), because
			#     BBBike cannot represent them (except for waterways & islands)
			my $new_members = process_multipolygon(\@members, $id);
			for my $member (@$new_members) {
			    if (($member->{role}||'') eq 'outer') {
				push @or, ["flaechen", "F:Forest", $member];
			    }
			}
		    }
		}

	    OUTPUT_RECORD: for my $or (@or) {
		    my($out_file, $cat, $member, %args) = @$or;

		    my $name = $name;
		    if (defined $args{name}) {
			$name = $args{name};
		    }

		    my $nodes;
		    if ($member->{'nodes'}) {
			$nodes = $member->{'nodes'};
		    } else {
			$nodes = $way2nodes{$member->{'ref'}};
		    }
		    if (!$nodes) {
			## Don't warn; it's usual that not all
			## relation members are part of the osm
			## download:
			#warn "Member '$member_id' in relation '$id' is missing";
		    } else {
			$out{$out_file} ||= [];
			my $out_array = $out{$out_file};
			my @coords = map { $node2ll{$_} } @$nodes;
			if ($out_array) {
			    # Check for continuations:
			    my $last_record = $out_array->[-1];
			    if ($last_record->[0] eq $name &&
				$last_record->[1][-1] eq $coords[0] &&
				$last_record->[2] eq $cat
			       ) {
				shift @coords;
				push @{$last_record->[1]}, @coords;
				next OUTPUT_RECORD;
			    }
			}
			push @$out_array, [$name, \@coords, $cat];
		    }
		}
	    }
	}
    }
}

######################################################################
# POST PROCESSING

resolve_node_fallback();
sort_by_population_and_category();
unhandled_stats();

######################################################################
# OUTPUT
if ($parsefor) {
    output_data(\*STDOUT, $out{"-"});
} else {
    if (!-d $o) {
	if (!mkdir $o) {
	    die "Cannot create output directory <$o>: $!" if !$f;
	}
    }
    chdir $o
	or die "Cannot chdir to $o: $!";
    while(my($filename,$data) = each %out) {
	print STDERR "$filename... " if $debug;
	open my $ofh, ">:raw:encoding($encoding)", "$filename~"
	    or die "Can't write to file <$filename~> in directory <$o>: $!";
	output_data($ofh, $data, $filename);
	close $ofh
	    or die "Error while closing: $!";
	rename "$filename~", $filename
	    or die "Can't rename $filename~ to $filename: $!";
	print STDERR "\n" if $debug;
    }

    # required files (dummy files, bbbike or bbbike.cgi like to have these):
    for my $file (qw(gesperrt gesperrt_car
		     wasserstrassen wasserumland wasserumland2 hoehe
		     orte orte2 qualitaet_l handicap_l
		     handicap_directed
		     berlin deutschland orte_city
		     landstrassen landstrassen2
		     ubahn ubahnhof sbahn sbahnhof rbahn rbahnhof
		     gesperrt_u gesperrt_s gesperrt_r
		     radwege_exact fragezeichen
		     Berlin.coords.data Potsdam.coords.data
		     plaetze ampeln green nolighting
		     culdesac faehren
		    )) {
	touch $file;
    }
    for (qw(cyclepath misc path route trafficjam tram kfzverkehr scenic ferry danger mount)) {
	touch "comments_$_";
    }
    # radwege will be used for plotting, radwege_exact for routing.
    # New bbbike can use the same file for both, but for hysterical
    # reasons still uses different files.
    if (!-e "radwege" && -e "radwege_exact") {
	if (!eval { symlink("",""); 1 }) {
	    warn "No support for symlinks on $^O, fallback to copy...\n";
	    require File::Copy;
	    File::Copy::cp("radwege_exact", "radwege")
		or warn "Cannot copy radwege_exact to radwege: $!";
	} else {
	    symlink "radwege_exact", "radwege"
		or warn "Cannot create symlink radwege -> radwege_exact: $!";
	}
    }
    if (!-d "temp_blockings") {
	mkdir "temp_blockings";
    }
    touch "temp_blockings/bbbike-temp-blockings.pl";
    output_meta();
}

#exit;

######################################################################

# Loosely following the algorithm ideas from
# http://wiki.openstreetmap.org/wiki/Relation:multipolygon/Algorithm
# Only the "Ring Assignment" steps are done here, and there's also
# no backtracking here.
sub process_multipolygon {
    my($members, $relation_id) = @_;

    my @new_members;
    my $mpc = MultipolygonCandidates->new;

    for my $member (@$members) {
	my $nodes = $way2nodes{$member->{ref}};
	next if !$nodes; # member may be ignored
	my $role = $member->{role};
	if ($nodes->[0] == $nodes->[-1]) { # way is closed
	    push @new_members, $member;
	} else {
	    $member->{nodes} = [@$nodes]; # copy nodes!
	    $mpc->add($member);
	}
    }

    while (!$mpc->is_empty) {
	my $member = $mpc->pick_one;
	$mpc->del($member);
	my $member_nodes = $member->{nodes};
	my $end_node = $member_nodes->[-1];
	my $another_member = $mpc->pick_one_end($end_node);
	if (!$another_member) {
	    if ($debug) {
		warn "multipolygon processing: member $member->{ref} in relation $relation_id looks incomplete, ignoring...\n";
	    }
	} else {
	    $mpc->del($another_member);
	    my $another_member_nodes = $another_member->{nodes};
	    if ($another_member_nodes->[0] == $end_node) {
		push @$member_nodes, @{ $another_member_nodes }[1..$#$another_member_nodes];
	    } else {
		my @reversed_another_member_nodes = reverse @$another_member_nodes;
		push @$member_nodes, @reversed_another_member_nodes[1..$#reversed_another_member_nodes];
	    }
	    if ($member_nodes->[0] == $member_nodes->[-1]) {
		push @new_members, $member;
	    } else {
		$mpc->add($member);
	    }
	}
    }

    return \@new_members;
}

# Decide whether U/S-Bahn is a real "public" railway or just a service
# one. Heuristics could work for Germany, but is currently restricted
# to Berlin.
#
# XXX srt (2013-01-06): this heuristics does not work anymore. service
# rails can now only be detected by checking relations, see relation
# id="2679163" for an example (U6)
sub is_rail_service {
    return 0; # XXX detection does not work anymore!!!

    my($type, $name, $nodes_ref) = @_;
    return 0 if ($type eq 'U' && $name =~ /^U\s*\d/);
    return 0 if ($type eq 'S' && $name =~ /^S\s*\d/);
    my($lon,$lat) = split /,/, $node2ll{$nodes_ref->[0]};
    my($east,$north) = split /,/, $conv->(14.525134, 52.826885);
    my($west,$south) = split /,/, $conv->(12.545141, 52.112717);
    return 0 if ($lat > $north || $lat < $south ||
		 $lon > $east  || $lon < $west); # outside Berlin and region
    return 1;
}

sub resolve_node_later {
    my($osm_layer, $name, $coord) = @_;
    $resolve_node_later{$osm_layer}->{$coord} = $name;
}

sub resolve_node_now {
    my($osm_layer, $bbbike_layer, $bbbike_cat, $nodes_ref) = @_;
    my $r = $resolve_node_later{$osm_layer};
    my $normalize_name = {ubahnhof => \&normalize_name_ubahnhof,
			  sbahnhof => \&normalize_name_sbahnhof,
			 }->{$bbbike_layer};
    do{warn "no $osm_layer layer"; return} if !$r;
    for my $coord (map { $node2ll{$_} } @$nodes_ref) {
	if (defined $coord && exists $r->{$coord}) {
	    my $name = $r->{$coord};
	    $name = $normalize_name->($name) if $normalize_name;
	    push @{$out{$bbbike_layer}}, [$name, [$coord], $bbbike_cat];
	    delete $r->{$coord};
	}
    }
}

sub resolve_node_fallback {
    my $have_unresolved = 0;
    for (keys %resolve_node_later) {
	if (keys %{$resolve_node_later{$_}}) {
	    $have_unresolved++; # XXX maybe, may be some already in %resolved
	    last;
	}
    }
    return if !$have_unresolved;

    print STDERR "unresolved:\n" if $debug;
    for my $osm_layer (keys %resolve_node_later) {
	print STDERR "  $osm_layer:\n" if $debug;
	while(my($coord, $name) = each %{ $resolve_node_later{$osm_layer} }) {
	    if ($osm_layer eq 'railway') {
		my($file, $cat);
 		if ($name =~ s{^U(|-Bhf)\s+}{}) {
		    ($file, $cat) = ("ubahnhof", "UA"); # zone is faked
 		} elsif ($name =~ s{^S(|-Bhf)\s+}{}) {
		    ($file, $cat) = ("sbahnhof", "SA");
 		} else {
		    ($file, $cat) = ("rbahnhof", "RA");
 		}
		push @{$out{$file}}, [$name, [$coord], $cat];
		print STDERR "    fallback for $name -> $cat $file\n" if $debug;
	    } else {
		print STDERR "    NO FALLBACK for $name ($coord)\n" if $debug;
	    }
	}
    }
}

sub normalize_name_ubahnhof {
    my $name = shift;
    $name =~ s{^U(\s+|-Bhf)\.?\s*}{};
    $name;
}

sub normalize_name_sbahnhof {
    my $name = shift;
    $name =~ s{^S(\s+|-Bhf)\.?\s*}{};
    $name;
}

sub output_addr {
    if (%addr) {
	while(my($name, $def) = each %addr) {
	    my $ll = exists $def->{ll} ? $def->{ll} : $node2ll{$def->{nodes}->[0]};
	    push @{$out{'_addr'}}, [$name, [$ll], 'HNR'];
	}
	%addr = (); # free memory
    }
}

sub sort_by_population_and_category {
    @{$out{"orte"}} = map {
	$_->[1]
    } sort {
	(defined $b->[0] && defined $a->[0] && $b->[0] <=> $a->[0]) ||
	    $b->[1][STRASSEN_CAT] <=> $a->[1][STRASSEN_CAT]
    } map {
	[$place_to_info{$_->[1][STRASSEN_NAME]}->{population} || undef, $_]
    } @{$out{"orte"}};
}

sub unhandled_stats {
    return if !%unhandled;

    my $max_key_len = max map { length } keys %unhandled;
    my $stats = "";

    $stats .= <<'EOF';
#
######################################################################
#
## Unhandled data statistics
#
## ... by count
#
EOF
    for my $key (sort { $unhandled{$b} <=> $unhandled{$a} || $a cmp $b } keys %unhandled) {
	$stats .= sprintf "# %-${max_key_len}s: %d\n", $key, $unhandled{$key};
    }
    $stats .= <<'EOF';
#
## ... alphabetically
#
EOF
    for my $key (sort keys %unhandled) {
	$stats .= sprintf "# %-${max_key_len}s: %d\n", $key, $unhandled{$key};
    }
    $additional_data_per_file{"_unhandled"} = $stats;
}

######################################################################
# output helpers

sub output_data {
    my($ofh, $data, $filename) = @_;
    print $ofh <<EOF;
#: #: -*- coding: $encoding -*-
EOF
    if ($encoding ne 'iso-8859-1') {
	print $ofh <<EOF;
#:encoding: $encoding
EOF
    }
    if (!$map) {
	print $ofh <<EOF;
#:map: polar
EOF
    } elsif ($map ne 'bbbike') {
	print $ofh <<EOF;
#:map: $map
EOF
    }
my $isodate = epoch2isodatez $newest_mtime;
$isodate = 'fixed' if $nodate;
print $ofh <<EOF;
#:date: $isodate
EOF
    if (defined $filename && $global_directives{$filename}) {
	for my $directive (@{ $global_directives{$filename} }) {
	    print $ofh <<EOF;
#:$directive
EOF
	}
    }

    # XXX really split long lines?
    my $do_splitlonglines = $splitlonglines && defined $filename && $file_needs_splitlonglines{$filename};

    my $p = basename($prog);
    print $ofh <<EOF;
#:
#
# OpenStreetMap data can be used freely under the terms of the 
# Creative Commons Attribution-ShareAlike 2.0 license.
# http://wiki.openstreetmap.org/wiki/OpenStreetMap_License
#
# Converted from openstreetmap data using $p v$VERSION
#
# DO NOT EDIT THIS FILE! Edit the original openstreetmap data!
#
EOF
    for my $rec (@$data) {
	my($name, $coords, $cat) = @$rec;
	$name =~ s{[\t\n]}{ }g;
	if (!defined $coords) {
	    # a directive
	    print $ofh $name, "\n";
	    next;
	}
	my $namecat = "$name\t$cat";
	my $coord_string = join(" ", grep { defined } @$coords);
	if ($do_splitlonglines && length($namecat) + length($coord_string) + 2 >= MAXLINELENGTH) {
	    warn_once("Long line detected in $filename, splitting...\n");
	    my $currlen = length($namecat);
	    print $ofh $namecat;
	    my $lastc;
	    for my $c (grep { defined } @$coords) {
		if ($currlen + 1 + length($c) + 1 > MAXLINELENGTH) {
		    print $ofh "\n$namecat $lastc";
		    $currlen = length($namecat);
		}
		print $ofh " ", $c;
		$lastc = $c;
		$currlen += 1 + length($c);
	    }
	    print $ofh "\n";
	} else {
	    print $ofh $namecat, " ", $coord_string, "\n";
	}
    }

    if (defined $filename) {
	if ($additional_data_per_file{$filename}) {
	    print $ofh $additional_data_per_file{$filename};
	}
    }

}

sub output_meta {
    my $meta_file_dd  = "meta.dd";
    my $meta_file_yml = "meta.yml";

    my %meta;

    $meta{created} = epoch2isodatez;
    $meta{created_by} = $ENV{EMAIL} || $ENV{REPLYTO} || $ENV{REPLY_TO} || eval { (getpwuid($<))[0] } || $ENV{USER} || '<unknown>';
    $meta{commandline} = \@commandline;
    $meta{creation_cwd} = $cwd;
    if (@bbox_wgs84) {
	@bbox_wgs84 = map { $_+0 } @bbox_wgs84;
	my @bbox;
	@bbox[0,1] = map { $_+0 } split /,/, $conv->(@bbox_wgs84[0,1]);
	@bbox[2,3] = map { $_+0 } split /,/, $conv->(@bbox_wgs84[2,3]);
	$meta{bbox} = \@bbox;
	$meta{bbox_wgs84} = \@bbox_wgs84;
    }
    if ($center_wgs84) {
	my $center = $conv->(split /,/, $center_wgs84);
	$meta{center} = [map { $_+0 } split /,/, $center];
	$meta{center_wgs84} = [map { $_+0 } split /,/, $center_wgs84];
    }
    # Simple country heuristic: use the country of largest city (if defined)
    for my $ort_def (@{ $out{"orte"} }) {
	my $country_code = $place_to_info{$ort_def->[1][STRASSEN_NAME]}->{'is_in:country_code'};
	if (defined $country_code) {
	    $meta{country} = $country_code;
	    last;
	}
    }
    $meta{source} = 'osm';
    #$meta{source_files} = \@osm_files; # XXX hmmm, too many?
    $meta{coordsys} = $map ? $map : 'wgs84';
    $meta{skip_features} = {green        => 1, # no support in osm data
			    obst         => 1, # -"-
			    landstrassen => 1, # no scoping with osm data
			    wasserumland => 1, # -"-
			    wideregion   => 1, # -"-
			    vbb          => 1, # even in Berlin, because we don't have the VBB zone info
			   };
    # following are skipped if no data was found
    for my $def (['u-bahn', ['ubahn', 'ubahnhof']],
		 ['s-bahn', ['sbahn', 'sbahnhof']],
		 ['r-bahn', ['rbahn', 'rbahnhof']],
		 ['tram', ['comments_tram']],
		 ['nolighting', ['nolighting']],
		 ['faehren', ['faehren']],
		) {
	my($feature_name, $layers) = @$def;
    CHECK_EMPTY_LAYER: {
	    for my $layer (@$layers) {
		last CHECK_EMPTY_LAYER if -s $layer;
	    }
	    $meta{skip_features}->{$feature_name} = 1;
	}
    }

    if ($no_create) {
	undef $meta{created};
	undef $meta{created_by};
	undef $meta{creation_cwd};
    }

    open my $ofh, ">", $meta_file_dd."~"
	or die "Cannot write to $meta_file_dd~: $!";
    print $ofh Data::Dumper->new([\%meta],['meta'])->Sortkeys(1)->Useqq(1)->Dump;
    close $ofh
	or die "While closing $meta_file_dd~: $!";
    rename $meta_file_dd."~", $meta_file_dd
	or die "While renaming $meta_file_dd~ to $meta_file_dd: $!";

    if (eval { require BBBikeYAML; 1 }) {
	BBBikeYAML::DumpFile($meta_file_yml, \%meta);
    } else {
	warn "No BBBikeYAML (or YAML::XS) available, fallback to possibly buggy YAML::Syck or YAML.pm...\n";
	if (eval { require YAML::Syck; 1 }) {
	    YAML::Syck::DumpFile($meta_file_yml, \%meta);
	} elsif (eval { require YAML; 1 }) {
	    YAML::DumpFile($meta_file_yml, \%meta);
	} else {
	    warn "No YAML::Syck or YAML.pm available, cannot dump $meta_file_yml.\n";
	    unlink $meta_file_yml; # may be an older file there
	}
    }
}

sub touch ($) {
    my $file = shift;
    sysopen my $fh, $file, O_WRONLY|O_CREAT
	or die "Can't create $file: $!";
    close $fh
	or die "Can't close $file: $!";
}

sub open_osm {
    my $osm_file = shift;
    my($dom, $reader);
    my $fh;
    if ($osm_file =~ m{\.gz$}) {
	if (!eval { require PerlIO::gzip; 1 }) {
	    open $fh, '-|', 'gzip', '-dc', $osm_file
		or die "Can't run zcat on $osm_file: $!";
	} else {
	    open $fh, "<:gzip", $osm_file
		or die "Can't open file $osm_file: $!";
	}
    } elsif ($osm_file =~ m{\.bz2$}) {
## See https://rt.cpan.org/Ticket/Display.html?id=42241
# 	if (!eval { require PerlIO::via::Bzip2; 1 }) {
# 	    die "No support for bzip2-compressed osm files, PerlIO::via::Bzip2 is missing.\n";
# 	}
# 	open $fh, "<:via(Bzip2)", $osm_file
# 	    or die "Can't open file $osm_file: $!";
	open $fh, "-|", "bunzip2", "--stdout", $osm_file
	    or die "Can't run bunzip2 on $osm_file: $!";
    }

    my @xmlparser;
    if ($xmlparser) {
	@xmlparser = $xmlparser;
    } else {
	@xmlparser = ('XML::LibXML::Reader', 'XML::LibXML');
    }

    for my $_try (@xmlparser) {
	if ($_try eq 'XML::LibXML::Reader') {
	    if (eval { require XML::LibXML::Reader; 1 }) {
		if ($fh) {
		    $reader = XML::LibXML::Reader->new(IO => $fh);
		} else {
		    $reader = XML::LibXML::Reader->new(location => $osm_file);
		}
		last;
	    }
	} elsif ($_try eq 'XML::LibXML') {
	    if (eval { require XML::LibXML; 1 }) {
		my $p = XML::LibXML->new;
		if ($fh) {
		    $dom = $p->parse_fh($fh)->documentElement;
		} else {
		    $dom = $p->parse_file($osm_file)->documentElement;
		}
		last;
	    }
	} else {
	    die "Unknown XML parser $_try";
	}
    }

    if (!$dom && !$reader) {
	if (!-e $osm_file) {
	    die "The file '$osm_file' does not exist.\n";
	} elsif (!-r $osm_file) {
	    die "The file '$osm_file' is not readable.\n";
	} else {
	    die "XML::LibXML has to be installed. Please fetch it from CPAN.\n";
	}
    }
    ($dom, $reader);
}

sub set_info_handler {
    my($osm_file, $dom, $reader) = @_;
    no warnings 'signal'; # INFO is usually only available on BSD systems
    $SIG{INFO} = sub {
	my $msg = "File $osm_file";
	if ($reader) {
	    no warnings 'uninitialized';
	    # Unfortunately, for compressed files byteConsumed is the
	    # position in the uncompressed stream, so it's not
	    # possible to determine the relative file position.
	    $msg .= sprintf ", bytes consumed %d, line %d, current node name '%s', id=%s", $reader->byteConsumed, $reader->lineNumber, $reader->name, $reader->getAttribute('id');
	}
	print STDERR $msg, "\n";
	print STDERR "Memory: ", join(", ", currmem()), "\n";
	require Carp; Carp::carp('Currently');
    };
}

sub parse_warning {
    my($reader, $where) = @_;
    require Carp;
    local $Carp::CarpLevel = $Carp::CarpLevel + 1;
    Carp::carp("*** PARSE WARNING: unexpected node <" . $reader->name . "> at line " . $reader->lineNumber . " $where");
}

{
    my %warn_once;
    sub warn_once {
	my $warning = join(" ", @_);
	return if $warn_once{$warning};
	warn $warning;
	$warn_once{$warning}++;
    }
}

sub epoch2isodatez (;$) {
    my $time = shift || time;
    my @l = gmtime $time;
    sprintf("%04d%02d%02d%02d%02d%02dZ",
	    $l[5]+1900, $l[4]+1, $l[3],
	    $l[2],      $l[1],   $l[0]);
}

# Return (minlot,minlat,maxlon,maxlat)
# Only works if XML::LibXML::Reader is available,
# and the <bounds> tag comes right after the <osm> tag
# (which is the case for osmconvert-generated files)
sub get_bounds_from_osm_file {
    my $file = shift;
    my(undef, $reader) = open_osm($file);
    if (!$reader) {
	die "Cannot get bounds with XML::LibXML::Reader";
    }
    $reader->nextElement;
    die "Expected <osm> tag" if $reader->name ne 'osm';
    $reader->nextElement;
    if ($reader->name ne 'bounds') {
	die "Expected <bounds>, but got <" . $reader->name . ">";
    }
    my @bbox = (
		$reader->getAttribute("minlon"),
		$reader->getAttribute("minlat"),
		$reader->getAttribute("maxlon"),
		$reader->getAttribute("maxlat"),
	       );
    @bbox;
}

sub fingerprint_and_exit {
    require Digest::MD5;
    my $dig = Digest::MD5->new;
    for my $f (@osm_files, (defined $height_db_file ? $height_db_file : ())) {
	open my $fh, $f
	    or die "Can't open $f: $!";
	binmode $fh;
	$dig->addfile($fh);
    }
    $dig->add(join("\0", @commandline));
    print $dig->hexdigest;
    exit 0;
}

# REPO BEGIN
# REPO NAME is_interactive /home/e/eserte/work/srezic-repository 
# REPO MD5 87e9e2500fbe4a3ffe5f977de8513d47
sub is_interactive {
    if ($^O eq 'MSWin32' || !eval { require POSIX; 1 }) {
	# fallback
	return -t STDIN && -t STDOUT;
    }

    # from perlfaq8
    open(TTY, "/dev/tty") or die $!;
    my $tpgrp = POSIX::tcgetpgrp(fileno(*TTY));
    my $pgrp = getpgrp();
    if ($tpgrp == $pgrp) {
	1;
    } else {
	0;
    }
}
# REPO END

# REPO BEGIN
# REPO NAME currmem /home/e/eserte/work/srezic-repository 
# REPO MD5 00fa532c42d6ade32b01e3fad34331a4

=item currmem([$pid])

=for category System

Return ($mem, $realmem) of the current process or process $pid, if $pid
is given.

On Linux, an even shorter alternative for getting the virtual memory is:

    cat /proc/$$/stat | cut -d" " -f 23

=cut

sub currmem {
    my $pid = shift || $$;
    no warnings 'portable'; # because of possible large hex values on 64bit systems
    if ($^O eq 'freebsd' && open(MAP, "dd if=/proc/$pid/map bs=64k 2>/dev/null |")) { # FreeBSD
	my $mem = 0;
	my $realmem = 0;
	while(<MAP>) {
	    my(@l) = split /\s+/;
	    my $delta = (hex($l[1])-hex($l[0]));
	    $mem += $delta;
	    if ($l[11] ne 'vnode') {
		$realmem += $delta;
	    }
	}
	close MAP;
	($mem, $realmem);
    } elsif ($^O eq 'linux' && open(MAP, "/proc/$pid/maps")) { # Linux
	my $mem = 0;
	my $realmem = 0;
	while(<MAP>) {
	    my(@l) = split /\s+/;
	    my($start,$end) = split /-/, $l[0];
	    my $delta = (hex($end)-hex($start));
	    $mem += $delta;
	    if (!defined $l[5] || $l[5] eq '') {
		$realmem += $delta;
	    }
	}
	close MAP;
	($mem, $realmem);
    } else {
	undef;
    }
}
# REPO END

{
    package MultipolygonCandidates;

    sub new {
	my($class) = @_;
	bless {ends => {}}, $class;
    }

    sub add {
	my($self, $member) = @_;
	my $nodes = $member->{nodes};
	my $ends = $self->{ends};
	$ends->{$nodes->[0]}->{$member} = $member;
	$ends->{$nodes->[-1]}->{$member} = $member;
    }
    sub del {
	my($self, $member) = @_;
	my $nodes = $member->{nodes};
	my $ends = $self->{ends};
	
	for my $node ($nodes->[0], $nodes->[-1]) {
	    delete $ends->{$node}->{$member};
	    $self->_check_empty($node);
	}
    }

    sub is_empty {
	my($self) = @_;
	!keys %{ $self->{ends} }; # check + reset
    }
    sub pick_one {
	my($self) = @_;
	my $ends = $self->{ends};
	my($node, $v) = _pick_hash_elem($ends);
	my(undef, $member) = _pick_hash_elem($v);
	$member;
    }

    sub pick_one_end {
	my($self, $node) = @_;
	my $ends = $self->{ends};
	if (!$ends->{$node} || !keys %{ $ends->{$node} }) {
	    return undef;
	}
	my($member_key, $member) = _pick_hash_elem($ends->{$node});
	return $member;
    }

    sub _check_empty {
	my($self, $node) = @_;
	my $ends = $self->{ends};
  	if (!keys %{ $ends->{$node} }) {
	    delete $ends->{$node};
	}
    }

    sub _pick_hash_elem {
	my($hashref) = @_;
	my($k,$v) = each %$hashref;
	keys %$hashref; # reset iterator
	($k,$v);
    }
}

__END__

=head1 NAME

osm2bbd - convert OpenStreetMap data for BBBike

=head1 SYNOPSIS

    ./osm2bbd [options] -o /path/to/output /path/to/osm/files

=head1 HOWTO

Create a directory where to download OSM data. It's best to use an
empty directory. Convention: I use
C<misc/download/osm/I<region_name>> as a path name. For Berlin, this
would be:

    cd /path/to/bbbike
    mkdir -p misc/download/osm/berlin

Use L<downloadosm> to download the OSM data. Decide first on the
bounding box to download (see also hints below). To load all of
Berlin, use

    ./miscsrc/downloadosm -o misc/download/osm/berlin 13.010982 52.337651 13.761388 52.675354

With a standard DSL connection, this last about 1:30 hours, totalling
220 MB of data (as of July 2008, in Januar 2009, it was nearly 400
MB). Note that it's fine to use also larger osm files (i.e. in the
size of a German state), e.g. the prepared files from
L<http://download.geofabrik.de>. Gzipped and bzipped osm files are fine,
but need C<zcat> resp. C<bunzip2> installed.

Now convert the OSM data into a BBBike-readable format. This should
take 5 minutes or so (Athlon64, i386-freebsd) for the above dataset.
Decide on the destination directory. Convention: I use
C<data_I<region_name>_osm_bbbike>. Note: this directory should not
exist. For Berlin, this would be:

    ./miscsrc/osm2bbd -v -map bbbike -o data_berlin_osm_bbbike/ misc/download/osm/berlin

Finally call bbbike with the C<-datadir> option:

    ./bbbike -datadir data_berlin_osm_bbbike

=head2 HINTS

To get the bbox of a bbd file, use:

    perl -l -MKarte::Standard -MKarte::Polar -MStrassen -e '$s=Strassen->new("data/strassen"); ($x1,$y1,$x2,$y2) = $s->bbox; print join(",", $Karte::Polar::obj->standard2map($x1,$y1),$Karte::Polar::obj->standard2map($x2,$y2))'

Get the visible area in the BBBike application. Start ptksh within
BBBike (in the Settings menu if bbbike was started with -advanced, orr
hit just Shift-P) and the type in:

    @corners = $c->get_corners;
    join(",", $Karte::Polar::obj->standard2map(anti_transpose(@corners[0,1])), 
              $Karte::Polar::obj->standard2map(anti_transpose(@corners[2,3])));

=head1 EXAMPLES

To convert data for the San Francisco area:

=over

=item 1. download geo data for San Francisco/SFO from opensteetmaps.org

    ./miscsrc/downloadosm -o ../sfbike/sfo -- -122.527 37.594 -122.351 37.811

=item 2. convert *.osm files to bbbike data

    ./miscsrc/osm2bbd -f -map bbbike -center -122.598,37.6829 -country us -o data-sfo ../sfbike/sfo

=item 3. (optional) do some additional postprocessing

    ./miscsrc/osm2bbd-postprocess data-sfo

=item 4. start sfbike, english version

    env LANG=en_US.UTF-8 perl ./bbbike -datadir data-sfo

=back

=head1 SUPPORTED LANGUAGES

If no language is specified, then output is optimized for German. The
only other language currently supported is English (C<en>).

=head1 SUPPORTED COUNTRIES

Currently there's only special handling for C<de>: motorways get
translated from "AI<number>" to "BABI<number>" to for handling with
BBBike.

=head1 EXPERIMENTS

The C<-experimental> switch turns experimental features on. Currently
are defined:

=over

=item C<add_postal_code>

Add the postal code (if available) to every street name. This helps in
cases when street names are ambigous (because the area is large enough
to cover multiple cities, or if the same street name exists multiple
times in the same city).

=item C<coastline_hack>

Prepare for better coastline handling. This needs additional support
from L<osm2bbd-postprocess>. See the C<-only-coastline-hack> and
C<-coastline-hack-anchor> options there.

=item C<polar_coord_hack>

Turn the polar coordinates hack on. This would create a rewritten
version of the module L<Karte::Polar>, which is adapted to the central
latitude of the processed region. This means distance calculation is
still not perfect, but far better for regions far away from the
reference latitude of BBBike (which is 52.5, the latitude of Berlin).
Currently only the Perl/Tk version has out-of-the-box support for this
hack. To use the hack in the CGI implementation it is necessary to add
something like

    unshift @INC, "/path/to/datadir;

best in the configuration file F<bbbike.cgi.config> (or whatever
config file is used).

=back

=head1 IMPLEMENTATION NOTES

There are two parsing modes, which can be switched by using the
C<-xmlparser> switch: C<XML::LibXML::Reader> and C<XML::LibXML>. The
C<XML::LibXML> parsing mode is more robust, but takes much more memory
especially for large osm files. The C<XML::LibXML::Reader> parsing
mode is work in progress but the only option to parse large osm files.

Currently C<XML::LibXML::Reader> is the default parsing mode.

=head1 BUGS/TODO

There are a lot of bugs and unsolved issues.

=over

=item * It is not clear whether to put cycle data to F<radwege> or
F<radwege_exact>. In the long run, F<radwege> should be completely
replaced by F<radwege_exact>.

=item * BBBike should be fixed to handle normal WGS84 coordinates for
everything. Currently the best we get is the C<polar_coord_hack>
mentioned in L</EXPERIMENTS>.

=item * Tunnel mound optimization needed! Problem visible in
Nord-Sd-Tunnel. Possible implementation: remember the end points with
the neighbor point (that is, for A B C D E remember A B and E D). This
structure points to the data record and is indexed by the end points.
Before doing the bbd output do another processing step: for every
remembered item check whether there is another item with the same end
point but another neighbor point. If such an item exists, then add the
"_" (no mound) attribute to the Tu category on the right side.

=item * It seems that "holes" in buildings are specified by just a
closed <way> without any name and other tag. Implementation
suggestion: put all the closed ways without any other tags into a
special bucket and output at the end of the corresponding layer.
Question: how to get the corresponding layer? Check for inclusion?

Update: there is usually also a relation with outer and inner roles.

=item * Look at the Reichstag. Get rid of the multiple stars (is this
a bbbike or osm2bbd task?)

=item * Currently the url tag will be output as a url directive into
the bbd file. Maybe use a info file instead, because currently the
info support in bbbike is better than the directive support? Or make
the directive support in bbbike better?

=back

=head1 AUTHOR

Slaven Rezic

=head1 SEE ALSO

L<osm2bbd-postprocess>, L<downloadosm>.

=cut

