OSM Map On Garmin/Postcode Search On Garmin

From OpenStreetMap Wiki
Jump to navigation Jump to search

Compiled maps for Garmin don't seem to have postcode search function. This is a hack to enable a UK postcode search set on a Garmin Nuvi 255W, and I hope it would work similairly with others. Hope it helps you, as this was (until now), my main bugbear with OSM on a Garmin. With this hack, postcode coverage is better that that I have with the original Garmin maps (now 12 months out of date), and I didn't want to waste my money on an upgrade to fix this with no guarantee it would be much better with their current generation UK map.

Step One

Download CodePoint Open list of postcodes and grid references from OrdinanceSurvey https://data.gov.uk/dataset/89a37a30-925c-4185-9ff9-c414dfb0de8b/code-point-open. Extract it wherever you like. Alternatively MySociety [1] make a version of CodePoint Open which has already been converted to WGS 84 lat/lon, which means step two might be unnecessary, and may save a bit of grief. SK53 23:11, 15 May 2011 (BST)

Step Two

Convert from eastings/northings to latitude/longitude. I used the Excel functions for doing this from Phil Brady http://www.haroldstreet.org.uk/osgb/excel-conversion-code/ (because I had them readily available), and ended up writing the following code to convert directly to a reduced CSV file.

  • Even if you have an unlocked version of his worksheet, I would suggest you create another workbook, add a VBA module to this (Alt-F11 to open the editor then Insert/Module, and paste the Conversion Code at the end of this page)
  • Edit the two directory lines to point to your codepointopen postcodes_gb\Data directory, and your desired output directory.
  • If you want to convert to GPI files with GPSBabel, then ensure bmpName points to a valid icon file, and uncomment the block of lines at the bottom which include oExec (uncommenting just means deleting the ' character from the beginning of the line).
  • Run the routine to create the simplified files (F5 while your cursor is in the createPostCodeFiles code). NB you will need both files open in Excel simultaneously.

Step Three

If you haven't already done it in the previous step, use GPSBabel or similar software to convert your desired postcode coverage files to .GPI format

Step Five

Backup your POIs

Step Four

Copy the resulting file/s to your garmin device (under the directory X:\Garmin\POI (either on the onboard memory or on the memory card). If you put it on the memory card when it first reboots it will ask you if you want to install it permanently on the device. Initially I'd suggest not until you've checked it works properly for you, as it's a pain to have to do a mass delete and reinstall of all the POIs.

Step Five

Search for your Postcode in the installed POIs. On the Nuvi this is via Where To/Extras/Custom POIs/All Categories/Spell. Not sure how it will work on others tho.

Conversion Code

Sub createPostCodeFiles()
    Dim eastings As Long, northings As Long
'    Dim latitud As double, longitude As double
    Dim osReference As String
    inputDirectory = "D:\Downloads\Downloads\codepointopen postcodes_gb\Data\"
    outputDirectory = "D:\Downloads\Downloads\codepointopen postcodes_gb\output\"
    bmpName = outputDirectory & "postcode.bmp"

    Const ForReading = 1, ForWriting = 2, ForAppending = 8
    Const TristateFalse = 0

    Set fso = CreateObject("Scripting.FileSystemObject")
    Set WshShell = CreateObject("WScript.Shell")
    Set oFolder = fso.GetFolder(inputDirectory)
    Set oFiles = oFolder.Files
        ' rename all the jpegs
    For Each Item In oFiles
        Debug.Print Item.Name
        inputName = inputDirectory & Item.Name
        outputName = outputDirectory & Item.Name
        gpiName = Replace(outputName, ".csv", ".gpi")
        Set curInputFile = fso.OpenTextFile(inputName, ForReading)
        Set curOutputFile = fso.OpenTextFile(outputName, ForWriting, True)
        ' Read from the file and display the results.
        Do While curInputFile.AtEndOfStream <> True
            TextLine = curInputFile.ReadLine
            ' Write to the file.
            postcode = Left(TextLine, InStr(TextLine, ",") - 1)
            postcode = Replace(postcode, " ", "")
            postcode = Replace(postcode, """", "")
            curLoc = instrOcc(TextLine, ",", 10)
            nextLoc = InStr(curLoc + 1, TextLine, ",")
            eastings = CLng(Mid(TextLine, curLoc + 1, nextLoc - curLoc - 1))
            curLoc = nextLoc
            nextLoc = InStr(curLoc + 1, TextLine, ",")
            northings = CLng(Mid(TextLine, curLoc + 1, nextLoc - curLoc - 1))
            osReference = MetresToOSref(eastings, northings)
            latitud = CDbl(latitude(84, osReference))
            longitud = CDbl(longitude(84, osReference))
            curOutputFile.WriteLine latitud & "," & longitud & "," & postcode
        ' your desired icon is the file listed in bmpName above
        shellStr = """C:\Program Files\GPSBabel\gpsbabel.exe"" -i csv -f """ & outputName & """ -o garmin_gpi,category=""Postcodes"",bitmap=""" & bmpName & """ -F """ & gpiName & """"
        Debug.Print shellStr
        ' If you have installed GPSBabel and want it to convert each file to gpi, then uncomment the following (delete the ' character from the beginning of the lines)
'        Set oExec = WshShell.Exec(shellStr)
'        Do While oExec.Status = 0
'            DoEvents
'        Loop
End Sub

Function instrOcc(stringToSearch, stringToFind, occurance)
    curLoc = 1
    For i = 1 To occurance
        curLoc = InStr(curLoc, stringToSearch, stringToFind) + 1
    curLoc = curLoc - 1
    instrOcc = curLoc
End Function

Alternative method

I have had successful results creating an OSM-format file and merging it with a downloaded OSM file using osmosis. Postcodes can then be searched for using Where To? -> Cities, and results are returned extremely quickly.

As a prerequisite it is necessary to be able to have the latitude and longitude of each postcode. Two possible mechanisms have already been described above.

The format of the file is as follows. This example shows a node corresponding to the fictitious postcode XX12 9TY at longitude 50.1234567°N 2.3456789°W.

<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6" generator="ANW">
<bounds minlat="49.7667486" minlon="-8.1628456" maxlat="60.8081270" maxlon="1.7604432"/>
<node id="1152921504606846976" lat="50.1234567" lon="-2.3456789" user="nobody" uid="1" visible="true" version="1" changeset="1" timestamp="2013-01-25T01:21:31Z" >
<tag k="amenity" v="postal_code"/>
<tag k="name" v="XX12 9TY"/>
<!-- additional node records, one per postcode -->

In the above it is important to note the following

  • The max/mins in the bounds record should encompass all postcodes' latitude and longitude;
  • Latitude and longitude must be to seven decimal places;
  • The node ID's need to be unique and preferably a few orders of magnitude higher than the highest value node ID in the OSM database. Good results have been had with node IDs from 1152921504606846976 (2^60) upwards. A conflict with an OSM node ID can cause several undesirable effects, such as a road extending in a straight line for the entire length of Britain if a postcode node id happens to conflict with a road node. The use of negative node IDs, whilst it used to avoid conflicts with OSM node IDs, does not seem to work with current builds of mkgmap, which are all fully 64-bit compliant and seem to expect a node ID to be an unsigned integer.
  • The attributes "user", "uid", (possibly) "visible", "version", "changeset" and "timestamp" are clearly of no significance as far as the resulting map is concerned, but mkgmap complained when one of these was absent. I did not investigate this further, instead I simply included all the attributes found in an OSM extract.
  • The postcode should ideally be formatted to have one (and only one) space between the outgoing and incoming part. The incoming part is always one digit and two letters. The OSGB extracts have postcodes "centre justified" in an 7-character field, for example "L1□□2AB", "AB1□2CD", "SW1A0AA". The ONS postcode file includes the postcode correctly formatted. A C# function to format a postcode is shown below - this includes a sanity check to avoid an exception if a short string is supplied to it.
        private static string FormatPostcode(String s)
            String s2 = s.Replace(" ", "");
            if (s2.Length >= 4)
                return s2.Substring(0, s2.Length - 3) + " " + s2.Substring(s2.Length - 3);
                return s;

In the "points" style file I resolved amenity:postal_code to a "hamlet", viz:

amenity=postal_code { name '${name}' } [0x0b00 resolution 24]

The reason for selecting this point type, is that hamlets are the smallest settlement returned in city searches. For best results, the TYP file should contain a transparent image for points of type 0x0b00.

In order to combine the postcode OSM file with a downloaded OSM extract, an osmosis command similar to the following can be used. This example assumes the postcode file is called ukpostcodes.osm, and it is being combined with the Geofabrik British Isles download.

osmosis --read-pbf british_isles.osm.pbf --read-xml ukpostcodes.osm --merge --sort --write-pbf british-isles-plus-postcodes.osm.pbf

Alternatively, osmconvert can be used, as follows. In this example, the combined file has the o5m format.

osmconvert british_isles.osm.pbf ukpostcodes.osm --drop-version --out-o5m > british-isles-plus-postcodes.o5m

Either way, the combined file can be processed, as normal, with splitter and mkgmap.

The resulting map will allow searches for post code using Where To? -> Cities. Searches for distant postcodes are more likely to work because city searches work worldwide (or at least as far as the basemap extends) whereas POI searches - even for built-in Garmin POI categories - seem to have geographical limitations. Also city searches seem to be better optimised than POI searches, so results will be returned more quickly.

When mkgmap is run specifying the bounds.zip file, all entities (including "postcode hamlets") will be allocated to the country, county and district specified in the bounds file.