﻿/*! Copyright © 2009-2011 Postcode Anywhere (Holdings) Ltd. (http://www.postcodeanywhere.co.uk)
 *
 * Address v1.00
 * Component for address lookup integrations.
 *
 * Luke 14/12/2011 12:34:42
 */

(function (window, undefined) {
    var _pca = window.pca = window.pca || {},
        _protocol = window.parent.location.protocol == "https:" ? "https:" : "http:",
        _host = "services.postcodeanywhere.co.uk",
        _endpoint = "json2.ws",
        _document = window.document;

    //Types
    _pca.CountryType = { ISO2: "iso2", ISO3: "iso3", NAME: "name" };
    _pca.ListType = { LIST: "List", SELECT: "Select", AUTOCOMPLETE: "AutoComplete", MODAL: "Modal" };
    _pca.ItemType = { STREET: "Street", TEXT: "Text" };
    _pca.AddressDataType = { RANGEDPREMISE: "RangedPremise", STREET: "Street", LOCALITY: "Locality", NONE: "None" };

    //Local Data
    _pca.request = "";
    _pca.response = "";

    _pca.synonyms = [
        { r: /\bN(?=\s)/, w: "NORTH" },
        { r: /\b(?:NE|NORTHEAST)(?=\s)/, w: "NORTH EAST" },
        { r: /\b(?:NW|NORTHWEST)(?=\s)/, w: "NORTH WEST" },
        { r: /\bS(?=\s)/, w: "SOUTH" },
        { r: /\b(?:SE|SOUTHEAST)(?=\s)/, w: "SOUTH EAST" },
        { r: /\b(?:SW|SOUTHWEST)(?=\s)/, w: "SOUTH WEST" },
        { r: /\bE(?=\s)/, w: "EAST" },
        { r: /\bW(?=\s)/, w: "WEST" },
        { r: /\bST(?=\s)/, w: "SAINT" }
    ];
    _pca.diacritics = [
        { r: /[ÀÁÂÃ]/gi, w: "A" },
        { r: /Å/gi, w: "AA" },
        { r: /[ÆæÄ]/gi, w: "AE" },
        { r: /Ç/gi, w: "C" },
        { r: /Ð/gi, w: "DJ" },
        { r: /[ÈÉÊË]/gi, w: "E" },
        { r: /[ÌÍÏ]/gi, w: "I" },
        { r: /Ñ/gi, w: "N" },
        { r: /[ÒÓÔÕ]/gi, w: "O" },
        { r: /[ŒØÖ]/gi, w: "OE" },
        { r: /Š/gi, w: "SH" },
        { r: /ß/gi, w: "SS" },
        { r: /[ÙÚÛ]/gi, w: "U" },
        { r: /Ü/gi, w: "UE" },
        { r: /[ŸÝ]/gi, w: "ZH" },
        { r: /-/gi, w: " " },
        { r: /[.,]/gi, w: "" }
    ];
    _pca.countries = [
        { iso2: "AF", iso3: "AFG", name: "Afghanistan" },
        { iso2: "AX", iso3: "ALA", name: "Åland Islands" },
        { iso2: "AL", iso3: "ALB", name: "Albania" },
        { iso2: "DZ", iso3: "DZA", name: "Algeria" },
        { iso2: "AS", iso3: "ASM", name: "American Samoa" },
        { iso2: "AD", iso3: "AND", name: "Andorra" },
        { iso2: "AO", iso3: "AGO", name: "Angola" },
        { iso2: "AI", iso3: "AIA", name: "Anguilla" },
        { iso2: "AQ", iso3: "ATA", name: "Antarctica" },
        { iso2: "AG", iso3: "ATG", name: "Antigua and Barbuda" },
        { iso2: "AR", iso3: "ARG", name: "Argentina" },
        { iso2: "AM", iso3: "ARM", name: "Armenia" },
        { iso2: "AW", iso3: "ABW", name: "Aruba" },
        { iso2: "AU", iso3: "AUS", name: "Australia" },
        { iso2: "AT", iso3: "AUT", name: "Austria" },
        { iso2: "AZ", iso3: "AZE", name: "Azerbaijan" },
        { iso2: "BS", iso3: "BHS", name: "Bahamas" },
        { iso2: "BH", iso3: "BHR", name: "Bahrain" },
        { iso2: "BD", iso3: "BGD", name: "Bangladesh" },
        { iso2: "BB", iso3: "BRB", name: "Barbados" },
        { iso2: "BY", iso3: "BLR", name: "Belarus" },
        { iso2: "BE", iso3: "BEL", name: "Belgium" },
        { iso2: "BZ", iso3: "BLZ", name: "Belize" },
        { iso2: "BJ", iso3: "BEN", name: "Benin" },
        { iso2: "BM", iso3: "BMU", name: "Bermuda" },
        { iso2: "BT", iso3: "BTN", name: "Bhutan" },
        { iso2: "BO", iso3: "BOL", name: "Bolivia, Plurinational State Of" },
        { iso2: "BQ", iso3: "BES", name: "Bonaire, Saint Eustatius and Saba" },
        { iso2: "BA", iso3: "BIH", name: "Bosnia and Herzegovina" },
        { iso2: "BW", iso3: "BWA", name: "Botswana" },
        { iso2: "BV", iso3: "BVT", name: "Bouvet Island" },
        { iso2: "BR", iso3: "BRA", name: "Brazil" },
        { iso2: "IO", iso3: "IOT", name: "British Indian Ocean Territory" },
        { iso2: "BN", iso3: "BRN", name: "Brunei Darussalam" },
        { iso2: "BG", iso3: "BGR", name: "Bulgaria" },
        { iso2: "BF", iso3: "BFA", name: "Burkina Faso" },
        { iso2: "BI", iso3: "BDI", name: "Burundi" },
        { iso2: "KH", iso3: "KHM", name: "Cambodia" },
        { iso2: "CM", iso3: "CMR", name: "Cameroon" },
        { iso2: "CA", iso3: "CAN", name: "Canada" },
        { iso2: "CV", iso3: "CPV", name: "Cape Verde" },
        { iso2: "KY", iso3: "CYM", name: "Cayman Islands" },
        { iso2: "CF", iso3: "CAF", name: "Central African Republic" },
        { iso2: "TD", iso3: "TCD", name: "Chad" },
        { iso2: "CL", iso3: "CHL", name: "Chile" },
        { iso2: "CN", iso3: "CHN", name: "China" },
        { iso2: "CX", iso3: "CXR", name: "Christmas Island" },
        { iso2: "CC", iso3: "CCK", name: "Cocos (Keeling) Islands" },
        { iso2: "CO", iso3: "COL", name: "Colombia" },
        { iso2: "KM", iso3: "COM", name: "Comoros" },
        { iso2: "CG", iso3: "COG", name: "Congo" },
        { iso2: "CD", iso3: "COD", name: "Congo, the Democratic Republic of the" },
        { iso2: "CK", iso3: "COK", name: "Cook Islands" },
        { iso2: "CR", iso3: "CRI", name: "Costa Rica" },
        { iso2: "CI", iso3: "CIV", name: "Côte D'ivoire" },
        { iso2: "HR", iso3: "HRV", name: "Croatia" },
        { iso2: "CU", iso3: "CUB", name: "Cuba" },
        { iso2: "CW", iso3: "CUW", name: "Curaçao" },
        { iso2: "CY", iso3: "CYP", name: "Cyprus" },
        { iso2: "CZ", iso3: "CZE", name: "Czech Republic" },
        { iso2: "DK", iso3: "DNK", name: "Denmark" },
        { iso2: "DJ", iso3: "DJI", name: "Djibouti" },
        { iso2: "DM", iso3: "DMA", name: "Dominica" },
        { iso2: "DO", iso3: "DOM", name: "Dominican Republic" },
        { iso2: "EC", iso3: "ECU", name: "Ecuador" },
        { iso2: "EG", iso3: "EGY", name: "Egypt" },
        { iso2: "SV", iso3: "SLV", name: "El Salvador" },
        { iso2: "GQ", iso3: "GNQ", name: "Equatorial Guinea" },
        { iso2: "ER", iso3: "ERI", name: "Eritrea" },
        { iso2: "EE", iso3: "EST", name: "Estonia" },
        { iso2: "ET", iso3: "ETH", name: "Ethiopia" },
        { iso2: "FK", iso3: "FLK", name: "Falkland Islands (Malvinas)" },
        { iso2: "FO", iso3: "FRO", name: "Faroe Islands" },
        { iso2: "FJ", iso3: "FJI", name: "Fiji" },
        { iso2: "FI", iso3: "FIN", name: "Finland" },
        { iso2: "FR", iso3: "FRA", name: "France" },
        { iso2: "GF", iso3: "GUF", name: "French Guiana" },
        { iso2: "PF", iso3: "PYF", name: "French Polynesia" },
        { iso2: "TF", iso3: "ATF", name: "French Southern Territories" },
        { iso2: "GA", iso3: "GAB", name: "Gabon" },
        { iso2: "GM", iso3: "GMB", name: "Gambia" },
        { iso2: "GE", iso3: "GEO", name: "Georgia" },
        { iso2: "DE", iso3: "DEU", name: "Germany" },
        { iso2: "GH", iso3: "GHA", name: "Ghana" },
        { iso2: "GI", iso3: "GIB", name: "Gibraltar" },
        { iso2: "GR", iso3: "GRC", name: "Greece" },
        { iso2: "GL", iso3: "GRL", name: "Greenland" },
        { iso2: "GD", iso3: "GRD", name: "Grenada" },
        { iso2: "GP", iso3: "GLP", name: "Guadeloupe" },
        { iso2: "GU", iso3: "GUM", name: "Guam" },
        { iso2: "GT", iso3: "GTM", name: "Guatemala" },
        { iso2: "GG", iso3: "GGY", name: "Guernsey" },
        { iso2: "GN", iso3: "GIN", name: "Guinea" },
        { iso2: "GW", iso3: "GNB", name: "Guinea-Bissau" },
        { iso2: "GY", iso3: "GUY", name: "Guyana" },
        { iso2: "HT", iso3: "HTI", name: "Haiti" },
        { iso2: "HM", iso3: "HMD", name: "Heard Island and Mcdonald Islands" },
        { iso2: "VA", iso3: "VAT", name: "Holy See (Vatican City State)" },
        { iso2: "HN", iso3: "HND", name: "Honduras" },
        { iso2: "HK", iso3: "HKG", name: "Hong Kong" },
        { iso2: "HU", iso3: "HUN", name: "Hungary" },
        { iso2: "IS", iso3: "ISL", name: "Iceland" },
        { iso2: "IN", iso3: "IND", name: "India" },
        { iso2: "ID", iso3: "IDN", name: "Indonesia" },
        { iso2: "IR", iso3: "IRN", name: "Iran, Islamic Republic Of" },
        { iso2: "IQ", iso3: "IRQ", name: "Iraq" },
        { iso2: "IE", iso3: "IRL", name: "Ireland" },
        { iso2: "IM", iso3: "IMN", name: "Isle of Man" },
        { iso2: "IL", iso3: "ISR", name: "Israel" },
        { iso2: "IT", iso3: "ITA", name: "Italy" },
        { iso2: "JM", iso3: "JAM", name: "Jamaica" },
        { iso2: "JP", iso3: "JPN", name: "Japan" },
        { iso2: "JE", iso3: "JEY", name: "Jersey" },
        { iso2: "JO", iso3: "JOR", name: "Jordan" },
        { iso2: "KZ", iso3: "KAZ", name: "Kazakhstan" },
        { iso2: "KE", iso3: "KEN", name: "Kenya" },
        { iso2: "KI", iso3: "KIR", name: "Kiribati" },
        { iso2: "KP", iso3: "PRK", name: "Korea, Democratic People's Republic of" },
        { iso2: "KR", iso3: "KOR", name: "Korea, Republic of" },
        { iso2: "KW", iso3: "KWT", name: "Kuwait" },
        { iso2: "KG", iso3: "KGZ", name: "Kyrgyzstan" },
        { iso2: "LA", iso3: "LAO", name: "Lao people's Democratic Republic" },
        { iso2: "LV", iso3: "LVA", name: "Latvia" },
        { iso2: "LB", iso3: "LBN", name: "Lebanon" },
        { iso2: "LS", iso3: "LSO", name: "Lesotho" },
        { iso2: "LR", iso3: "LBR", name: "Liberia" },
        { iso2: "LY", iso3: "LBY", name: "Libya" },
        { iso2: "LI", iso3: "LIE", name: "Liechtenstein" },
        { iso2: "LT", iso3: "LTU", name: "Lithuania" },
        { iso2: "LU", iso3: "LUX", name: "Luxembourg" },
        { iso2: "MO", iso3: "MAC", name: "Macao" },
        { iso2: "MK", iso3: "MKD", name: "Macedonia, the Former Yugoslav Republic of" },
        { iso2: "MG", iso3: "MDG", name: "Madagascar" },
        { iso2: "MW", iso3: "MWI", name: "Malawi" },
        { iso2: "MY", iso3: "MYS", name: "Malaysia" },
        { iso2: "MV", iso3: "MDV", name: "Maldives" },
        { iso2: "ML", iso3: "MLI", name: "Mali" },
        { iso2: "MT", iso3: "MLT", name: "Malta" },
        { iso2: "MH", iso3: "MHL", name: "Marshall Islands" },
        { iso2: "MQ", iso3: "MTQ", name: "Martinique" },
        { iso2: "MR", iso3: "MRT", name: "Mauritania" },
        { iso2: "MU", iso3: "MUS", name: "Mauritius" },
        { iso2: "YT", iso3: "MYT", name: "Mayotte" },
        { iso2: "MX", iso3: "MEX", name: "Mexico" },
        { iso2: "FM", iso3: "FSM", name: "Micronesia, Federated States of" },
        { iso2: "MD", iso3: "MDA", name: "Moldova, Republic of" },
        { iso2: "MC", iso3: "MCO", name: "Monaco" },
        { iso2: "MN", iso3: "MNG", name: "Mongolia" },
        { iso2: "ME", iso3: "MNE", name: "Montenegro" },
        { iso2: "MS", iso3: "MSR", name: "Montserrat" },
        { iso2: "MA", iso3: "MAR", name: "Morocco" },
        { iso2: "MZ", iso3: "MOZ", name: "Mozambique" },
        { iso2: "MM", iso3: "MMR", name: "Myanmar" },
        { iso2: "NA", iso3: "NAM", name: "Namibia" },
        { iso2: "NR", iso3: "NRU", name: "Nauru" },
        { iso2: "NP", iso3: "NPL", name: "Nepal" },
        { iso2: "NL", iso3: "NLD", name: "Netherlands" },
        { iso2: "NC", iso3: "NCL", name: "New Caledonia" },
        { iso2: "NZ", iso3: "NZL", name: "New Zealand" },
        { iso2: "NI", iso3: "NIC", name: "Nicaragua" },
        { iso2: "NE", iso3: "NER", name: "Niger" },
        { iso2: "NG", iso3: "NGA", name: "Nigeria" },
        { iso2: "NU", iso3: "NIU", name: "Niue" },
        { iso2: "NF", iso3: "NFK", name: "Norfolk Island" },
        { iso2: "MP", iso3: "MNP", name: "Northern Mariana Islands" },
        { iso2: "NO", iso3: "NOR", name: "Norway" },
        { iso2: "OM", iso3: "OMN", name: "Oman" },
        { iso2: "PK", iso3: "PAK", name: "Pakistan" },
        { iso2: "PW", iso3: "PLW", name: "Palau" },
        { iso2: "PS", iso3: "PSE", name: "Palestinian Territory, Occupied" },
        { iso2: "PA", iso3: "PAN", name: "Panama" },
        { iso2: "PG", iso3: "PNG", name: "Papua New Guinea" },
        { iso2: "PY", iso3: "PRY", name: "Paraguay" },
        { iso2: "PE", iso3: "PER", name: "Peru" },
        { iso2: "PH", iso3: "PHL", name: "Philippines" },
        { iso2: "PN", iso3: "PCN", name: "Pitcairn" },
        { iso2: "PL", iso3: "POL", name: "Poland" },
        { iso2: "PT", iso3: "PRT", name: "Portugal" },
        { iso2: "PR", iso3: "PRI", name: "Puerto Rico" },
        { iso2: "QA", iso3: "QAT", name: "Qatar" },
        { iso2: "RE", iso3: "REU", name: "Réunion" },
        { iso2: "RO", iso3: "ROU", name: "Romania" },
        { iso2: "RU", iso3: "RUS", name: "Russian Federation" },
        { iso2: "RW", iso3: "RWA", name: "Rwanda" },
        { iso2: "BL", iso3: "BLM", name: "Saint Barthélemy" },
        { iso2: "SH", iso3: "SHN", name: "Saint Helena, Ascension and Tristan da Cunha" },
        { iso2: "KN", iso3: "KNA", name: "Saint Kitts and Nevis" },
        { iso2: "LC", iso3: "LCA", name: "Saint Lucia" },
        { iso2: "MF", iso3: "MAF", name: "Saint Martin (French part)" },
        { iso2: "PM", iso3: "SPM", name: "Saint Pierre and Miquelon" },
        { iso2: "VC", iso3: "VCT", name: "Saint Vincent and the Grenadines" },
        { iso2: "WS", iso3: "WSM", name: "Samoa" },
        { iso2: "SM", iso3: "SMR", name: "San Marino" },
        { iso2: "ST", iso3: "STP", name: "Sao Tome and Principe" },
        { iso2: "SA", iso3: "SAU", name: "Saudi Arabia" },
        { iso2: "SN", iso3: "SEN", name: "Senegal" },
        { iso2: "RS", iso3: "SRB", name: "Serbia" },
        { iso2: "SC", iso3: "SYC", name: "Seychelles" },
        { iso2: "SL", iso3: "SLE", name: "Sierra Leone" },
        { iso2: "SG", iso3: "SGP", name: "Singapore" },
        { iso2: "SX", iso3: "SXM", name: "Sint Maarten (Dutch part)" },
        { iso2: "SK", iso3: "SVK", name: "Slovakia" },
        { iso2: "SI", iso3: "SVN", name: "Slovenia" },
        { iso2: "SB", iso3: "SLB", name: "Solomon Islands" },
        { iso2: "SO", iso3: "SOM", name: "Somalia" },
        { iso2: "ZA", iso3: "ZAF", name: "South Africa" },
        { iso2: "GS", iso3: "SGS", name: "South Georgia and the South Sandwich Islands" },
        { iso2: "SS", iso3: "SSD", name: "South Sudan" },
        { iso2: "ES", iso3: "ESP", name: "Spain" },
        { iso2: "LK", iso3: "LKA", name: "Sri Lanka" },
        { iso2: "SD", iso3: "SDN", name: "Sudan" },
        { iso2: "SR", iso3: "SUR", name: "Suriname" },
        { iso2: "SJ", iso3: "SJM", name: "Svalbard and Jan Mayen" },
        { iso2: "SZ", iso3: "SWZ", name: "Swaziland" },
        { iso2: "SE", iso3: "SWE", name: "Sweden" },
        { iso2: "CH", iso3: "CHE", name: "Switzerland" },
        { iso2: "SY", iso3: "SYR", name: "Syrian Arab Republic" },
        { iso2: "TW", iso3: "TWN", name: "Taiwan, Province Of China" },
        { iso2: "TJ", iso3: "TJK", name: "Tajikistan" },
        { iso2: "TZ", iso3: "TZA", name: "Tanzania, United Republic Of" },
        { iso2: "TH", iso3: "THA", name: "Thailand" },
        { iso2: "TL", iso3: "TLS", name: "Timor-Leste" },
        { iso2: "TG", iso3: "TGO", name: "Togo" },
        { iso2: "TK", iso3: "TKL", name: "Tokelau" },
        { iso2: "TO", iso3: "TON", name: "Tonga" },
        { iso2: "TT", iso3: "TTO", name: "Trinidad and Tobago" },
        { iso2: "TN", iso3: "TUN", name: "Tunisia" },
        { iso2: "TR", iso3: "TUR", name: "Turkey" },
        { iso2: "TM", iso3: "TKM", name: "Turkmenistan" },
        { iso2: "TC", iso3: "TCA", name: "Turks and Caicos Islands" },
        { iso2: "TV", iso3: "TUV", name: "Tuvalu" },
        { iso2: "UG", iso3: "UGA", name: "Uganda" },
        { iso2: "UA", iso3: "UKR", name: "Ukraine" },
        { iso2: "AE", iso3: "ARE", name: "United Arab Emirates", alternates: [ "UAE" ] },
        { iso2: "GB", iso3: "GBR", name: "United Kingdom", alternates: [ "Britain", "England", "Great Britain", "Northern Ireland", "Scotland", "UK", "Wales" ] },
        { iso2: "US", iso3: "USA", name: "United States", alternates: [ "America", "United States of America" ] },
        { iso2: "UM", iso3: "UMI", name: "United States Minor Outlying Islands" },
        { iso2: "UY", iso3: "URY", name: "Uruguay" },
        { iso2: "UZ", iso3: "UZB", name: "Uzbekistan" },
        { iso2: "VU", iso3: "VUT", name: "Vanuatu" },
        { iso2: "VE", iso3: "VEN", name: "Venezuela, Bolivarian Republic Of" },
        { iso2: "VN", iso3: "VNM", name: "Viet Nam" },
        { iso2: "VG", iso3: "VGB", name: "Virgin Islands, British" },
        { iso2: "VI", iso3: "VIR", name: "Virgin Islands, U.S." },
        { iso2: "WF", iso3: "WLF", name: "Wallis and Futuna" },
        { iso2: "EH", iso3: "ESH", name: "Western Sahara" },
        { iso2: "YE", iso3: "YEM", name: "Yemen" },
        { iso2: "ZM", iso3: "ZMB", name: "Zambia" },
        { iso2: "ZW", iso3: "ZWE", name: "Zimbabwe" }
    ];

    //Simplified service call method takes service name as a string, params as an object, success as callback function(response) and error as callback function(message)
    _pca.fetch = function (service, params, success, error, cache) {
        var _script = _pca.create("script"),
            _head = _document.getElementsByTagName("head")[0],
            _request = _protocol + "//" + _host + "/" + service + "/" + _endpoint,
            _params = "";

        //Handle the response
        function handleResponse(response) {
            if (response.length === 1 && response[0].Error !== undefined)
                error(response[0].Description);
            else
                success(response);
        }

        params = params || {};
        success = success || function () { };
        error = error || function (description) { _pca.fire("error", description); };

        for (var i in params)
            _params += (_params ? "&" : "?") + i + "=" + encodeURIComponent(params[i]);

        _params += "&CallbackVariable=pca.response";
        _request += _params;

        if (cache && _request === _pca.request)
            handleResponse(_pca.response);
        else {
            _script.src = _request;

            _script.onload = _script.onreadystatechange = function () {
                if (!this.readyState || this.readyState === "loaded" || this.readyState === "complete") {
                    _script.onload = _script.onreadystatechange = null;
                    if (_head && _script.parentNode)
                        _head.removeChild(_script);

                    _pca.request = _request;
                    handleResponse(_pca.response);
                }
            }

            _pca.request = "";
            _pca.response = "";
            _head.insertBefore(_script, _head.firstChild);
        }
    }

    _pca.Object = function (source) {
        var _this = source || this;

        _this.listeners = {};

        //Listens to a PCA event
        _this.listen = function (event, action) {
            _this.listeners[event] = _this.listeners[event] || [];
            _this.listeners[event].push(action);
        }

        //Fires an event
        _this.fire = function (event, data) {
            if (_this.listeners[event]) {
                for (var i = 0; i < _this.listeners[event].length; i++)
                    _this.listeners[event][i](data);
            }
        }

        return _this;
    }

    //A list object to display multiple results and select with filtering and sorting
    _pca.List = function () {
        var _list = new _pca.Object();

        _list.element = _pca.create("div", { className: "pcalist" });
        _list.defaultClass = "pcalistitem pcadisableselect";
        _list.items = [];
        _list.count = 0;
        _list.selected = null;
        _list.scroll = {
            held: false,
            moved: false,
            origin: 0,
            position: 0,
            x: 0,
            y: 0,
            dx: 0,
            dy: 0
        }

        function createItem(item, format, type, callback) {
            var _item = new _pca.Object(),
                _text = _pca.trimSpaces(_pca.formatLine(item, format)),
                _type = type || _pca.ItemFormat.TEXT;

            _item.element = _pca.create("li", { innerHTML: _text, className: _list.defaultClass });
            _item.data = item;
            _item.text = _text;
            _item.type = _type;
            _item.tag = (_type == _pca.ItemType.STREET ? _pca.formatStreetTag(_text) : _pca.formatTag(_text));
            _item.filter = false;

            _item.select = function () {
                if (!_list.scroll.moved) _list.select(_item.data, callback);
            }

            _item.highlight = function () {
                _list.highlight(_item.element);
            }

            _pca.listen(_item.element, "mouseover", _item.highlight);
            _pca.listen(_item.element, "click", _item.select);
            _pca.listen(_item.element, "mousedown", _list.mouseDown);

            _list.element.appendChild(_item.element);
            _list.items.push(_item);

            return _item;
        }

        //Add an item or array of items to the list
        _list.add = function (data, format, callback) {
            //If the object is an array then add all of the items, otherwise just add the item
            if (data.constructor == Array) {
                for (var i = 0; i < data.length; i++)
                    createItem(data[i], format, _pca.ItemType.TEXT, callback);
            }
            else
                createItem(data, format, _pca.ItemType.TEXT, callback);

            _list.count = _list.element.childNodes.length;

            return _list;
        }

        //Add an item or array of items to the list
        _list.addStreets = function (data, format, callback) {
            //If the object is an array then add all of the items, otherwise just add the item
            if (data.constructor == Array) {
                for (var i = 0; i < data.length; i++)
                    createItem(data[i], format, _pca.ItemType.STREET, callback);
            }
            else
                createItem(data, format, _pca.ItemType.STREET, callback);

            _list.count = _list.element.childNodes.length;

            return _list;
        }

        //Clears the list and then populates the item(s)
        _list.populate = function (data, format, callback) {
            _list.clear();
            _list.add(data, format, callback);
            return _list;
        }

        //Clears the list and then populates the item(s)
        _list.populateStreets = function (data, format, callback) {
            _list.clear();
            _list.addStreets(data, format, callback);
            return _list;
        }

        //Redraws the list, usually done when items have changed, been sorted or filtered
        _list.draw = function () {
            while (_list.element.childNodes.length)
                _list.element.removeChild(_list.element.childNodes[0]);

            _list.clearHighlight();
            _list.count = 0;

            for (var i = 0; i < _list.items.length; i++) {
                if (!_list.items[i].filter) {
                    _list.element.appendChild(_list.items[i].element);
                    _list.count++;
                }
            }

            return _list;
        }

        _list.select = function (item, callback) {
            callback(item);

            return _list;
        }

        _list.filter = function (pattern) {
            var _tag = _pca.formatTag(pattern),
                _streetTag = _pca.formatStreetTag(pattern);

            for (var i = 0; i < _list.items.length; i++)
                _list.items[i].filter = _list.items[i].type == _pca.ItemType.STREET ? _list.items[i].tag.indexOf(_streetTag) != 0 : _list.items[i].tag.indexOf(_tag) != 0;

            _list.fire("filter", pattern);

            return _list;
        }

        _list.sort = function (reverse, field) {
            var _field = field || "text";

            if (_list[_field])
                _list.items.sort(function (a, b) { return (reverse ? a[_field] > b[_field] : a[_field] < b[_field]) });
            else
                _list.items.sort(function (a, b) { return (reverse ? a.data[_field] > b.data[_field] : a.data[_field] < b.data[_field]) });

            _list.fire("sort");

            return _list;
        }

        _list.clear = function () {
            _list.clearHighlight();
            _list.items = [];

            while (_list.element.childNodes.length)
                _list.element.removeChild(_list.element.childNodes[0]);

            _list.count = 0;
            _list.hide();
            _list.fire("clear");

            return _list;
        }

        _list.setScroll = function (position) {
            _list.element.scrollTop = position;
        }

        //Mouse events
        _list.mouseDown = function (event) {
            var _event = event || window.event;

            _list.scroll.held = true;
            _list.scroll.moved = false;
            _list.scroll.origin = parseInt(_list.element.scrollTop);
            _list.scroll.y = parseInt(_event.screenY);
        }

        _list.mouseOut = function () {
            _list.scroll.held = false;
        }

        _list.mouseUp = function () {
            _list.scroll.held = false;
        }

        _list.mouseMove = function (event) {
            if (_list.scroll.held) {
                var _event = event || window.event;

                _list.scroll.dy = _list.scroll.y - parseInt(_event.screenY);
                _list.scroll.position = _list.scroll.origin + _list.scroll.dy;
                _list.setScroll(_list.scroll.position);
                _list.scroll.moved = true;
                _event.returnValue = false;
            }
        }

        //Touch events
        _list.touchStart = function (event) {
            var _event = event || window.event;

            _list.scroll.held = true;
            _list.scroll.moved = false;
            _list.scroll.origin = parseInt(_list.scrollTop);
            _list.scroll.y = parseInt(_event.touches[0].pageY);
        }

        _list.touchEnd = function () {
            _list.scroll.held = false;
        }

        _list.touchCancel = function () {
            _list.scroll.held = false;
        }

        _list.touchMove = function (event) {
            if (_list.scroll.held) {
                var _event = event || window.event;

                //Disable Gecko and Webkit image drag
                if (_event.preventDefault)
                    _event.preventDefault();

                _list.scroll.dy = _list.scroll.y - parseInt(_event.touches[0].pageY);
                _list.scroll.position = _list.scroll.origin + _list.scroll.dy;
                _list.setScroll(_list.scroll.position);
                _list.scroll.moved = true;
                _event.returnValue = false;
            }
        }

        //Keyboard events
        _list.keySelect = function (key) {
            switch (key) {
                case 40: //Down
                    if (!_list.selectedItem)
                        _list.selectFirst();
                    else
                        _list.selectNext();
                    return true;
                case 38: //Up
                    if (!_list.selectedItem)
                        _list.selectLast();
                    else
                        _list.selectPrevious();
                    return true;
                case 13: //Enter/Return
                    if (_list.selectedItem) {
                        for (var i = 0; i < _list.items.length; i++) {
                            if (_list.items[i].element == _list.selectedItem) {
                                _list.items[i].select();
                                break;
                            }
                        }
                    }
                    return true;
                case 27: //Escape
                    _list.hide();
                    return true;
            }

            return false;
        }

        _list.selectFirst = function () {
            if (_list.element.childNodes.length)
                _list.highlight(_list.element.childNodes[0], true);
        }

        _list.selectLast = function () {
            if (_list.element.childNodes.length)
                _list.highlight(_list.element.childNodes[_list.element.childNodes.length - 1], true);
        }

        _list.selectNext = function () {
            if (_list.selectedItem && _list.selectedItem.nextSibling)
                _list.highlight(_list.selectedItem.nextSibling, true);
            else
                _list.selectFirst();
        }

        _list.selectPrevious = function () {
            if (_list.selectedItem && _list.selectedItem.previousSibling)
                _list.highlight(_list.selectedItem.previousSibling, true);
            else
                _list.selectLast();
        }

        _list.clearHighlight = function () {
            if (_list.selectedItem)
                _list.selectedItem.className = _list.defaultClass;

            _list.selectedItem = null;
        }

        _list.highlight = function (element, scrollTo) {
            if (_list.selectedItem)
                _list.selectedItem.className = _list.defaultClass;

            element.className = _list.defaultClass + " pcaselected";
            _list.selectedItem = element;

            if (scrollTo) _list.scrollToItem(element);

            return _list;
        }

        _list.scrollToItem = function (element) {
            _list.scroll.position = _list.element.scrollTop;

            if (element.offsetTop < _list.scroll.position) {
                _list.scroll.position = element.offsetTop;
                _list.setScroll(_list.scroll.position);
            }
            else {
                if (element.offsetTop + element.offsetHeight > _list.scroll.position + _list.element.offsetHeight) {
                    _list.scroll.position = element.offsetTop + element.offsetHeight - _list.element.offsetHeight;
                    _list.setScroll(_list.scroll.position);
                }
            }

            return _list;
        }

        _pca.listen(_document, "mouseup", _list.mouseUp);
        _pca.listen(_document, "mousemove", _list.mouseMove);

        _pca.listen(_list.element, "touchstart", _list.touchStart);
        _pca.listen(_list.element, "touchmove", _list.touchMove);
        _pca.listen(_list.element, "touchend", _list.touchEnd);
        _pca.listen(_list.element, "touchcancel", _list.touchCancel);

        return _list;
    }

    //Creates an autocomplete which filters on the field value
    //Inherits from list
    _pca.AutoComplete = function (options) {
        var _autocomplete = new _pca.List(),
            _anchor = _pca.create("table", { className: "pcaautocomplete pcatext", cellPadding: 0, cellSpacing: 0 }),
            _chain = [_anchor.insertRow(0).insertCell(0), _anchor.insertRow(1).insertCell(0)],
            _options = options || {};

        _autocomplete.field = null;
        _autocomplete.visible = false;
        _autocomplete.mouseover = false;
        _autocomplete.focus = false;

        _autocomplete.anchor = function (field) {
            _autocomplete.field = _pca.getInput(field);

            _autocomplete.field.parentNode.insertBefore(_anchor, _autocomplete.field);
            _chain[0].appendChild(_autocomplete.field);
            _chain[1].appendChild(_autocomplete.element);

            _pca.listen(_autocomplete.field, "keyup", _autocomplete.keyup);
            _pca.listen(_autocomplete.field, "focus", _autocomplete.focused);
            _pca.listen(_autocomplete.field, "blur", _autocomplete.blurred);

            return _autocomplete;
        }

        _autocomplete.show = function () {
            if (_autocomplete.count) {
                _autocomplete.visible = true;
                _autocomplete.element.style.display = "";
                _autocomplete.setScroll(0);
            }
            return _autocomplete;
        }

        _autocomplete.hide = function () {
            _autocomplete.visible = false;
            _autocomplete.element.style.display = "none";
            return _autocomplete;
        }

        _autocomplete.focused = function () {
            _autocomplete.focus = true;
            _autocomplete.filter(_pca.getValue(_autocomplete.field)).draw().show();
        }

        _autocomplete.blurred = function () {
            _autocomplete.focus = false;

            if (!_autocomplete.mouseover)
                _autocomplete.hide();
        }

        _autocomplete.keyup = function (event) {
            var _key = window.event ? window.event.keyCode : event.which;

            if (_autocomplete.visible) {
                if (!_autocomplete.keySelect(_key)) {
                    _autocomplete.filter(_pca.getValue(_autocomplete.field)).draw();
                    if (!_autocomplete.count) _autocomplete.hide();
                    _autocomplete.fire("keyup", _key);
                }
            }
            else if (_key == 40) //Key down to show list
                _autocomplete.show();
            else if (_key == 8 || _key == 46) //Backspace or Delete
                _autocomplete.filter(_pca.getValue(_autocomplete.field)).draw().show();
            else
                _autocomplete.fire("keyup", _key);
        }

        if (_options.field) _autocomplete.anchor(_options.field);
        _pca.listen(_anchor, "mouseover", function () { _autocomplete.mouseover = true });
        _pca.listen(_anchor, "mouseout", function () { _autocomplete.mouseover = false });
        _autocomplete.hide();

        return _autocomplete;
    }

    //Creates a modal popup window to display results
    //Inherits from list
    _pca.Modal = function (options) {
        var _modal = new _pca.List(),
            _options = options || {},
            _mask = _pca.create("div", { className: "pcamask pcafullscreen" }),
            _container = _pca.create("div", { className: "pcamodal pcatext" }),
            _border = _pca.create("div", { className: "pcaborder" }),
            _background = _pca.create("div", { className: "pcabackground" }),
            _header = _pca.create("div", { className: "pcaheader" }),
            _headertext = _pca.create("div", { className: "pcatext" }),
            _footer = _pca.create("div", { className: "pcafooter" });

        _modal.visible = false;

        _modal.show = function () {
            _mask.style.display = "";
            _container.style.display = "";
            _container.visible = true;
            _modal.centre();
            _modal.setScroll(0);
            return _modal;
        }

        _modal.hide = function () {
            _mask.style.display = "none";
            _container.style.display = "none";
            _container.visible = false;
            return _modal;
        }

        //position the modal popup in the centre of the window
        _modal.centre = function () {
            _container.style.left = (Math.max(_document.documentElement.clientWidth, _document.body.scrollWidth, _document.body.offsetWidth) / 2) - (_container.offsetWidth / 2) + "px";
            _container.style.top = (Math.max(_document.documentElement.clientHeight, _document.body.scrollHeight, _document.body.offsetHeight) / 2) - (_container.offsetHeight / 2) + "px";
            return _modal;
        }

        _modal.select = function (item, callback) {
            callback(item);
            return _modal;
        }

        _headertext.innerHTML = _options.title || "";

        _document.body.appendChild(_mask);
        _document.body.appendChild(_container);
        _container.appendChild(_border);
        _container.appendChild(_background);
        _header.appendChild(_headertext);
        _background.appendChild(_header);
        _background.appendChild(_modal.element);
        _background.appendChild(_footer);

        _pca.listen(_mask, "click", _modal.hide);
        _modal.hide();

        return _modal;
    }

    //Creates a select list with countries in
    _pca.CountryAutoComplete = function (options) {
        var _countryAutoComplete = new _pca.AutoComplete(options);

        _countryAutoComplete.select = function (country) {
            pca.setValue(_countryAutoComplete.field, country.name);
            _countryAutoComplete.hide();
        }

        _countryAutoComplete.populate = function (filter) {
            var _filter = typeof (filter) == "string" && filter ? filter.split(",") : filter || [];

            if (_filter.length) {
                for (var i = 0; i < _filter.length; i++) {
                    for (var c = 0; c < _pca.countries.length; c++) {
                        if (_filter[i] == _pca.countries[c].iso3)
                            _countryAutoComplete.add(_pca.countries[c], "{name}", _countryAutoComplete.select);
                    }
                }
            }
            else {
                for (var c = 0; c < _pca.countries.length; c++)
                    _countryAutoComplete.add(_pca.countries[c], "{name}", _countryAutoComplete.select);
            }

            return _countryAutoComplete;
        }

        _countryAutoComplete.selectedCountry = function () {
            return _pca.getCountry(_countryAutoComplete.field);
        }

        _countryAutoComplete.populate(options.filter);

        return _countryAutoComplete;
    }

    //Creates a select list, can be used to select country or address options
    _pca.Select = function () {
        var _select = new _pca.Object();

        _select.element = _pca.create("select", { className: "pcaselect" });
        _select.callback = typeof callback == 'function' ? callback : function () { };

        _select.populate = function (data, textFormat, valueFormat) {
            for (var i = 0; i < data.length; i++)
                _select.add(data[i], textFormat, valueFormat);

            return _select;
        }

        _select.add = function (item, textFormat, valueFormat) {
            _select.element.options.add(new Option(_pca.formatLine(item, textFormat), _pca.formatLine(item, valueFormat)));
            return _select;
        }

        _select.clear = function () {
            _select.element.options.length = 0;
            return _select;
        }

        _select.select = function (value) {
            for (var i = 0; i < _select.element.options.length; i++) {
                if (_select.element.options[i].value == value || _select.element.options[i].text == value) {
                    _select.element.selectedIndex = i;
                    break;
                }
            }

            return _select;
        }

        _select.reset = function () {
            _select.element.selectedIndex = 0;
            return _select;
        }

        _select.selectedValue = function () {
            return _select.element.options[_select.element.selectedIndex].value;
        }

        _select.selectedText = function () {
            return _select.element.options[_select.element.selectedIndex].text;
        }

        _pca.listen(_select.element, 'change', function () { _select.fire("change", _select.selectedValue); });

        return _select;
    }

    //Creates a select list with countries in
    _pca.CountrySelect = function (options) {
        var _countrySelect = new _pca.Select();

        _countrySelect.field = null;
        _countrySelect.format = options.format || _pca.CountryType.NAME;

        _countrySelect.selectedCountry = function () {
            return _pca.getCountry(_countrySelect.selectedValue());
        }

        _countrySelect.updateField = function () {
            _pca.setValue(_countrySelect.field, _countrySelect.selectedCountry()[_countrySelect.format]);

            return _countrySelect;
        }

        _countrySelect.syncCountry = function (value) {
            _countrySelect.select(_pca.getCountry(value).iso3);
            _countrySelect.updateField();

            return _countrySelect;
        }

        _countrySelect.populate = function (filter) {
            var _filter = typeof (filter) == "string" && filter ? filter.split(",") : filter || [];

            if (_filter.length) {
                for (var i = 0; i < _filter.length; i++) {
                    for (var c = 0; c < _pca.countries.length; c++) {
                        if (_filter[i] == _pca.countries[c].iso3)
                            _countrySelect.add(_pca.countries[c], "{name}", "{iso3}");
                    }
                }
            }
            else {
                for (var c = 0; c < _pca.countries.length; c++)
                    _countrySelect.add(_pca.countries[c], "{name}", "{iso3}");
            }

            return _countrySelect;
        }

        _countrySelect.replace = function (field) {
            _countrySelect.field = _pca.getInput(field);
            _countrySelect.field.parentNode.insertBefore(_countrySelect.element, _countrySelect.field);
            _countrySelect.field.style.display = "none";

            _countrySelect.syncCountry(_pca.getValue(_countrySelect.field))

            _countrySelect.listen('change', _countrySelect.updateField)

            return _countrySelect;
        }

        _countrySelect.populate(options.filter);
        if (options.field) _countrySelect.replace(options.field);

        return _countrySelect;
    }

    //Draw an advanced search form with the given fields, calls the callback with the field values
    _pca.Advanced = function (fields, callback) {
        var _form = new _pca.Object();

        _form.element = _pca.create("div", { className: "pcaform" });

        //draw fields

        //add button for search
    }

    //Draw a confirmation window, calls the callback upon confirmation
    _pca.Confirm = function (html, callback) {
        var _confirm = new _pca.Object();

        _confirm.element = _pca.create("div", { className: "pcaconfirm" });

        //draw the html

        //add button for confirm and cancel
    }

    //Create a command button
    _pca.Button = function (text, func) {
        var _button = new _pca.Object();

        _button.element = _pca.create("input", { type: "button", value: text, className: "pcabutton", onclick: func });

        return _button;
    }

    //Create a command link
    _pca.Link = function (text, func) {
        var _link = new _pca.Object();

        _link.element = _pca.create("a", { innerHTML: text, onclick: func, className: "pcatext pcalink" });

        return _link;
    }

    //Formats a line by replacing tags in the form {Property} with the corresponding property value or method result from the item object
    _pca.formatLine = function (item, format) {
        return format.replace(/\{(\w+)\}/g, function (m, c) { return (typeof item[c] == 'function' ? item[c]() : item[c]) || ""; });
    }

    //Formats a line into a simplified tag for filtering
    _pca.formatTag = function (line) {
        return line ? _pca.replaceList(_pca.removeHtml(line.toUpperCase()), _pca.diacritics) : "";
    }

    //Formats a line into a simplified tag for filtering with additonal helpers for street names
    _pca.formatStreetTag = function (line) {
        return _pca.replaceList(_pca.formatTag(line), _pca.synonyms);
    }

    //Performs all replacements in a list
    _pca.replaceList = function (line, list) {
        for (var i = 0; i < list.length; i++)
            line = line.replace(list[i].r, list[i].w);
        return line;
    }

    //Removes HTML tags from a string
    _pca.removeHtml = function (line) {
        return line.replace(/<[\w\/]+>/g, "");
    }

    //Removes unnecessary spaces
    _pca.trimSpaces = function (line) {
        return line.replace(/^\s+|\s(?=\s)|\s$/g, "");
    }

    //Gets the first words from a string
    _pca.getText = function (line) {
        return /[a-zA-Z][a-zA-Z\s]+[a-zA-Z]/.exec(line);
    }

    //Gets the first number from a string
    _pca.getNumber = function (line) {
        return /\d+/.exec(line);
    }

    //Merges one objects properties into another
    _pca.merge = function (source, destination) {
        for (var i in source)
            if (!destination[i]) destination[i] = source[i];
    }

    //Gets the country name from
    _pca.getCountry = function (reference) {
        if (!reference || typeof reference != 'string')
            return "";

        reference = reference.toUpperCase();

        for (var i = 0; i < _pca.countries.length; i++) {
            var country = _pca.countries[i];

            if (reference == country.iso2.toUpperCase() || reference == country.iso3.toUpperCase() || reference == country.name.toUpperCase())
                return country;
            else if (country.alternates) {
                for (var a = 0; a < country.alternates.length; a++) {
                    if (reference == country.alternates[a].toUpperCase())
                        return country;
                }
            }
        }
    }

    //Find a DOM element from a pca reference
    _pca.getInput = function (reference, base) {
        var _tags = ['input', 'textarea', 'select'],
            _matches = [],
            _base = base || _document,
            _exact = null;

        if (!reference || reference == "")
            return null;

        if (reference.tagName) {
            for (var t = 0; t < _tags.length; t++) {
                if (reference.tagName == _tags[t].toUpperCase())
                    return reference;
            }
        }

        _exact = _base.getElementById(reference);

        if (_exact)
            return _exact;

        for (var t = 0; t < _tags.length; t++) {
            var _fields = _base.getElementsByTagName(_tags[t]);

            for (var f in _fields) {
                var _id = _fields[f].id || "",
                    _name = _fields[f].name || "";

                if (_fields[f] == reference)
                    return _fields[f];
                else if (typeof (reference) == 'string' && _id) {
                    if (_id == reference)
                        return _fields[f];

                    if (_id.indexOf(reference) > 0)
                        _matches.push(_fields[f]);
                }
                else if (typeof (reference) == 'string' && _name) {
                    if (_name == reference)
                        return _fields[f];
                }
            }
        }

        if (_matches.length > 0)
            return _matches[0];
        else
            return null;
    }

    //Get the value of a DOM element
    _pca.getValue = function (element) {
        if (element) {
            if (!element.tagName) {
                element = _pca.getInput(element);
                if (!element) return "";
            }

            if (element.tagName == "INPUT" || element.tagName == "TEXTAREA") {
                if (element.type == "text" || element.type == "textarea")
                    return element.value;
                if (element.type == "checkbox")
                    return element.checked;
            }
            if (element.tagName == "SELECT")
                return element.options[element.selectedIndex].value;
        }

        return "";
    }

    //Set the value of a DOM element
    _pca.setValue = function (element, value) {

        _pca.debug(element + " - " + value);

        if (element && (value || value == '')) {
            if (!element.tagName) {
                element = _pca.getInput(element);
                if (!element) return;
            }

            if (element.tagName == "INPUT" || element.tagName == "TEXTAREA") {
                if (element.type == "text" || element.type == "textarea")
                    element.value = value;
                if (element.type == "checkbox")
                    element.checked = ((typeof (value) == "boolean" && value) || value == "True");
            }
            if (element.tagName == "SELECT") {
                for (var s = 0; s < element.options.length; s++) {
                    if (element.options[s].value == value || element.options[s].text == value) {
                        element.selectedIndex = s;
                        break;
                    }
                }
            }
        }
    }

    //Simple function to create an element
    _pca.create = function (tag, properties, cssText) {
        var _elem = _document.createElement(tag)
        for (var i in properties || {})
            _elem[i] = properties[i];
        if (cssText) _elem.style.cssText = cssText;
        return _elem;
    }

    //Listens to an event with standard DOM event handling
    _pca.listen = function (target, event, action) {
        if (window.addEventListener)
            target.addEventListener(event, action, false);
        else
            target.attachEvent('on' + event, action);
    }

    //PCA debug messages. Just add a div with an id of "pcadebug"
    _pca.debug = function (message) {
        var _degbug = document.getElementById("pcadebug");

        if (_degbug)
            _degbug.innerHTML = message;
    }
})(window);

(function (window, undefined) {
    var _pca = window.pca,
        _document = window.document;

    //Main address component
    _pca.Address = function (fields, options) {
        var _this = new _pca.Object(this);

        //messages
        _this.messages = {
            NORESULTS: "Sorry, no results were found.",
            SELECTADDRESS: "Select Your Address"
        };

        _this.templates = {
            INTERNATIONALPOSTCODE: "{Description}, {District} {City} {State}",
            INTERNATIONALSTREET: "{Street}, {District} {City} {State} {PostalCode}",
            INTERNATIONALBUILDING: "{Description}",
            INTERNATIONALADDRESS: "{Description}, {City}, {State}, {PostalCode}",
            ROYALMAILPOSTCODE: "{StreetAddress}, {Place}",
            ROYALMAILSTREET: "{Street}, {Place}",
            ROYALMAILBUILDING: "{StreetAddress}",
            USPSZIP4: "{Company} {Line1}, {City} {StateCode}"
        };

        //options
        _this.key = options.key || "";
        _this.cache = typeof options.cache == 'boolean' ? options.cache : true;

        _this.serviceOptions = options.serviceOptions || {};
        _this.serviceOptions.international = typeof _this.serviceOptions.international == 'boolean' ? _this.serviceOptions.international : true;
        _this.serviceOptions.royalMail = typeof _this.serviceOptions.royalMail == 'boolean' ? _this.serviceOptions.royalMail : false;
        _this.serviceOptions.usps = typeof _this.serviceOptions.usps == 'boolean' ? _this.serviceOptions.usps : false;

        _this.resultOptions = options.resultOptions || {};
        _this.resultOptions.type = _this.resultOptions.type || _pca.ListType.MODAL;
        _this.resultOptions.field = _this.resultOptions.field || "";
        _this.resultOptions.title = _this.resultOptions.title || _this.messages.SELECTADDRESS;

        _this.countryOptions = options.countryOptions || {};
        _this.countryOptions.format = _this.countryOptions.format || _pca.CountryType.NAME;
        _this.countryOptions.type = _this.countryOptions.type || _pca.ListType.SELECT;
        _this.countryOptions.field = _this.countryOptions.field || "";
        _this.countryOptions.filter = _this.countryOptions.filter || "";

        //lists
        _this.resultList = null;
        _this.countryList = null;

        //initialises the control
        _this.setup = function () {
            _this.resultList = new pca[_this.resultOptions.type](_this.resultOptions);

            if (_this.resultOptions.type == _pca.ListType.AUTOCOMPLETE)
                _this.resultList.listen("keyup", _this.checkForBuildingNumber);

            if (_this.countryOptions.field)
                _this.countryList = new _pca["Country" + _this.countryOptions.type](_this.countryOptions);
        }

        _this.fetch = function (service, params, callback) {
            function success(response) {
                _this.lastResponse = response;

                if (response.length)
                    callback(response);
                else {
                    _this.resultList.clear();
                    _this.fire("error", _this.messages.NORESULTS);
                }
            }

            function error(description) {
                _this.lastResponse = [];
                _this.fire("error", description);
            }

            _pca.fetch(service, params, success, error, _this.cache);
        }

        //Calculate the value of an address line
        _this.getAddressValue = function (reference, address) {
            var _addrLine;

            //Formats an address line using the PCA format
            function formatLine(format, address) {
                var _formatNumber = parseInt(format.substring(9, 10)),
                    _formatCount = parseInt(format.substring(11, 12)),
                    _seperateCompany = format.length < 15 || !address.Company,
                    _lineCount = 0,
                    _result = "";

                if (!address.Line1)
                    _lineCount = 0
                else if (!address.Line2)
                    _lineCount = 1
                else if (!address.Line3)
                    _lineCount = 2
                else if (!address.Line4)
                    _lineCount = 3
                else if (!address.Line5)
                    _lineCount = 4
                else
                    _lineCount = 5;

                if (!_seperateCompany)
                    _lineCount++;

                //work out the current line number and how many address elements should appear on it
                var _start = Math.floor((_lineCount / _formatCount) * (_formatNumber - 1)) + 1,
                    _lines = (Math.floor((_lineCount / _formatCount) * _formatNumber)) - (_start - 1);

                if (!_seperateCompany)
                    _start--;

                //concatenate the address elements to make the address line
                for (var a = 0; a < _lines; a++) {
                    if (!(a + _start))
                        _result += address.Company || "";
                    else
                        _result += (_result ? ", " : "") + (address["Line" + (a + _start)] || "");
                }

                return _result;
            }

            //Work out which values to use
            switch (reference.toUpperCase()) {
                case "ID":
                case "PAF-ID":
                case "UDPRN":
                    _addrLine = address.Udprn;
                    break;
                case "COMPANY":
                case "PAF-COMPANY":
                    _addrLine = address.Company;
                    break;
                case "BUILDINGNUMBER":
                case "PAF-BUILDING-NUMBER":
                    _addrLine = address.BuildingNumber || address.Building;
                    break;
                case "BUILDINGNAME":
                case "PAF-BUILDING-NAME":
                    _addrLine = address.BuildingName;
                    break;
                case "BUILDING":
                case "PAF-BUILDING":
                    _addrLine = (address.SubBuilding + ' ' + address.BuildingName + ' ' + address.BuildingNumber).replace(/^\s+/, '') || address.Building;
                    break;
                case "PAF-FLAT":
                    _addrLine = address.SubBuilding;
                    break;
                case "PAF-BUILDING-NAME-NUMBER":
                    _addrLine = (address.BuildingName + ' ' + address.BuildingNumber).replace(/^\s+/, '') || address.Building;
                    break;
                case "STREET":
                    _addrLine = address.Description || address.Street || address.PrimaryStreet;
                    break;
                case "PAF-STREET":
                    _addrLine = (address.SecondaryStreet == "" ? address.PrimaryStreet : (address.SecondaryStreet + ", " + address.PrimaryStreet).replace(/^\s+/, '')) || address.Street;
                    break;
                case "DISTRICT":
                case "PAF-DISTRICT":
                    _addrLine = address.DependentLocality || address.District;
                    break;
                case "CITY":
                case "TOWN":
                case "PAF-TOWN":
                    _addrLine = address.City || address.PostTown;
                    break;
                case "STATE":
                case "STATECODE":
                case "COUNTY":
                case "PAF-COUNTY":
                    _addrLine = address.State || address.StateCode || address.County;
                    break;
                case "STATENAME":
                    _addrLine = address.State || address.StateName || address.County;
                    break;
                case "ADMIN":
                case "ADMINAREA":
                case "US-COUNTY":
                    _addrLine = address.AdminArea || address.CountyName;
                    break;
                case "POSTALCODE":
                case "POSTCODE":
                case "ZIP":
                case "ZIP4":
                case "PAF-POSTCODE":
                    _addrLine = address.PostalCode || address.Postcode || address.Zip;
                    break;
                case "COUNTRY":
                case "PAF-COUNTRY":
                    _addrLine = _pca.getCountry(address.CountryIso3 || address.Country || address.CountryName)[_this.countryOptions.format];
                    break;
                case "COUNTRYNAME":
                    _addrLine = _pca.getCountry(address.CountryIso3 || address.Country || address.CountryName).name;
                    break;
                case "COUNTRYCODE":
                case "COUNTRYISO3":
                    _addrLine = _pca.getCountry(address.CountryIso3 || address.Country || address.CountryName).iso3;
                    break;
                case "COUNTRYISO2":
                    _addrLine = _pca.getCountry(address.CountryIso3 || address.Country || address.CountryName).iso2;
                    break;
                default:
                    if (/^PAF-LINE-[\d]-[\d]/.test(reference))
                        _addrLine = formatLine(reference, address);
                    else
                        _addrLine = address[reference];
                    break;
            }

            return _addrLine;
        }

        //Adds Line1, Line2 to international address
        _this.addAddressLines = function (addresses) {
            for (var i = 0; i < addresses.length; i++) {
                var address = addresses[i];
                address.Line1 = address.Description || address.Street;
                address.Line2 = address.District;
            }
        }

        //Waits for a building number to be entered into the street/line1 field
        _this.checkForBuildingNumber = function () {
            var _building = /^\d+(?=\s$)/.exec(_pca.getValue(_this.resultList.field).toString());
            if (_building) _this.fire("building", _building);
        }

        //Search using generic find services
        _this.search = function (line1, locality, postcode, country) {
            var _line1 = line1 || "",
                _building = _pca.getNumber(_line1) || "",
                _street = _pca.getText(_line1) || "",
                _locality = locality || "",
                _postcode = postcode || "",
                _country = country ? _pca.getCountry(country).iso3 : _this.countryList.selectedCountry().iso3;

            if (_postcode && (!_street || (_country != "GBR" && _country != "USA"))) //Temp fix for lack of international by address
                _this.searchByPostcode(_country, _postcode, _building);
            else if ((_street && !(_postcode || _locality)) || (_country != "GBR" && _country != "USA")) //Temp fix for lack of international by address
                _this.searchByStreet(_country, _street, _building);
            else if (_line1 && (_postcode || _locality)) {
                var _terms = [_line1];
                if (_locality) _terms.push(_locality);
                if (_postcode) _terms.push(_postcode);
                _this.searchByAddress(_country, _terms.join(","));
            }
        }

        //Searches for a whole address
        _this.searchByAddress = function (country, address) {
            var _country = country ? _pca.getCountry(country).iso3 : _this.countryList.selectedCountry().iso3,
                _address = address || "";

            function royalMailCleanseResult(response) {
                if (response.length == 1)
                    _this.select(response[0]);

                _this.resultList.clear();
            }

            function uspsAddressResult(response) {
                if (response.length == 1)
                    _this.select(response[0]);

                _this.resultList.clear();
            }

            if (_country == "GBR" && _this.serviceOptions.royalMail)
                _this.fetch("CleansePlus/Interactive/Cleanse/v1.00", { Key: _this.key, Address: _address }, royalMailCleanseResult);
            else if (_country == "USA" && _this.serviceOptions.usps)
                _this.fetch("PostcodeAnywhereInternational/InteractiveUSA/RetrieveByAddress/v1.00", { Key: _this.key, Address: _address }, uspsAddressResult);
        }

        //Searches for an address by postcode using building information
        _this.searchByPostcode = function (country, postcode, building) {
            var _country = country ? _pca.getCountry(country).iso3 : _this.countryList.selectedCountry().iso3,
                _postcode = postcode || "",
                _building = building || "";

            if (_country == "" || _postcode == "")
                return;

            function internationalSelectStreet(address) {
                _this.select(address);

                //If the description does not contain building numbers then search for them
                if (address.Street == address.Description)
                    _this.searchBuildings(address.StreetId, address);
                else {
                    //Append the Zip+4 if not present
                    if (_country == "USA" && _this.serviceOptions.usps && /^[0-9]{5}$/.test(address.PostalCode))
                        _this.searchByAddress(_country, _pca.formatLine(address, _this.templates.INTERNATIONALADDRESS));
                    else
                        _this.resultList.clear();
                }
            }

            function internationalStreetResults(response) {
                _this.addAddressLines(response);

                if (response.length == 1)
                    internationalSelectStreet(response[0]);
                else
                    _this.resultList.populateStreets(response, _this.templates.INTERNATIONALPOSTCODE, internationalSelectStreet).show();
            }

            function royalMailRetrieveResult(response) {
                _this.select(response[0]);
            }

            function royalMailSelectBuilding(item) {
                _this.fetch("PostcodeAnywhere/Interactive/RetrieveById/v1.20", { Key: _this.key, Id: item.Id }, royalMailRetrieveResult);
                _this.resultList.clear();
            }

            function royalMailStreetResults(response) {
                if (response.length == 1)
                    royalMailSelectAddress(response[0]);
                else
                    _this.resultList.populateStreets(response, _this.templates.ROYALMAILPOSTCODE, royalMailSelectBuilding).sort().draw().show();
            }

            function uspsZipResult(response) {
                if (response.length == 1)
                    uspsSelectAddress(response[0]);
                else
                    _this.resultList.populateStreets(response, _this.templates.USPSZIP4, uspsSelectAddress).show();
            }

            function uspsSelectAddress(address) {
                _this.select(address);
                _this.resultList.clear();
            }

            if (_country == "GBR" && _this.serviceOptions.royalMail)
                _this.fetch("PostcodeAnywhere/Interactive/FindByPostcode/v1.00", { Key: _this.key, Postcode: _postcode }, royalMailStreetResults);
            else if (_country == "USA" && _this.serviceOptions.usps && /[0-9]{5}-[0-9]{4}/.test(_postcode))
                _this.fetch("PostcodeAnywhereInternational/InteractiveUSA/RetrieveByZip4/v1.00", { Key: _this.key, Country: _country, Zip4: _postcode }, uspsZipResult);
            else if (_this.serviceOptions.international)
                _this.fetch("PostcodeAnywhereInternational/Interactive/RetrieveByPostalCode/v2.10", { Key: _this.key, Country: _country, PostalCode: _postcode, Building: _building }, internationalStreetResults);
        }

        //Searches for an address by street using building and city information
        _this.searchByStreet = function (country, street, building) {
            var _country = country ? _pca.getCountry(country).iso3 : _this.countryList.selectedCountry().iso3,
                _street = street || "",
                _building = building || "";

            if (_country == "" || _street == "")
                return;

            function internationalSelectStreet(address) {
                _this.select(address);

                //If the description does not contain building numbers then search for them
                if (address.Street == address.Description)
                    _this.searchBuildings(address.StreetId, address);
                else {
                    //Append the Zip+4 if not present
                    if (_country == "USA" && _this.serviceOptions.usps && /^[0-9]{5}$/.test(address.PostalCode))
                        _this.searchByAddress(_country, _pca.formatLine(address, _this.templates.INTERNATIONALADDRESS));
                    else
                        _this.resultList.clear();
                }
            }

            function internationalStreetResults(response) {
                _this.addAddressLines(response);

                if (response.length == 1)
                    internationalSelectStreet(response[0]);
                else
                    _this.resultList.populateStreets(response, _this.templates.INTERNATIONALSTREET, internationalSelectStreet).show();
            }

            function royalMailSelectStreet(address) {
                _this.searchBuildings(address.StreetId);
                _this.resultList.clear();
            }

            function royalMailStreetResults(response) {
                if (response.length == 1)
                    royalMailSelectStreet(response[0]);
                else
                    _this.resultList.populateStreets(response, _this.templates.ROYALMAILSTREET, royalMailSelectStreet).show();
            }

            if (_country == "GBR" && _this.serviceOptions.royalMail)
                _this.fetch("PostcodeAnywhere/Interactive/FindStreets/v1.00", { Key: _this.key, Street: _street }, royalMailStreetResults);
            else if (_this.serviceOptions.international)
                _this.fetch("PostcodeAnywhereInternational/Interactive/RetrieveByStreet/v1.40", { Key: _this.key, Country: _country, Street: _street, Building: _building }, internationalStreetResults);
        }

        //Gets a list of buildings from a street Id
        _this.searchBuildings = function (streetId, address) {
            var _country = (/^\d+\|\d+\|\d+\|\d+$/.test(streetId) ? "GBR" : /^[A-Z]{3}(?=\|)/.exec(streetId).toString()) || "",
                _address = address || {};

            function internationalSelectBuilding(building) {
                _pca.merge(_address, building);
                _this.select(building);
                _this.resultList.clear();

                //Append the Zip+4 if not present
                if (_country == "USA" && _this.serviceOptions.usps && /^[0-9]{5}$/.test(building.PostalCode))
                    _this.searchByAddress(_country, _pca.formatLine(building, _this.templates.INTERNATIONALADDRESS));
            }

            function internationalBuildingResults(response) {
                _this.addAddressLines(response);

                if (response.length == 1)
                    internationalSelectBuilding(response[0]);
                else
                    _this.resultList.populateStreets(response, _this.templates.INTERNATIONALBUILDING, internationalSelectBuilding).show();
            }

            function royalMailRetrieveResult(response) {
                _this.select(response[0]);
            }

            function royalMailSelectBuilding(item) {
                _this.fetch("PostcodeAnywhere/Interactive/RetrieveById/v1.20", { Key: _this.key, Id: item.Id }, royalMailRetrieveResult);
                _this.resultList.clear();
            }

            function royalMailBuildingResults(response) {
                if (response.length == 1)
                    royalMailSelectBuilding(response[0]);
                else
                    _this.resultList.populateStreets(response, _this.templates.ROYALMAILBUILDING, royalMailSelectBuilding).show();
            }

            if (/^\d\|\d{5}\|\d{4}\|\d/.test(streetId))
                _this.fetch("PostcodeAnywhere/Interactive/FindByStreetId/v1.00", { Key: _this.key, StreetId: streetId }, royalMailBuildingResults);
            else
                _this.fetch("PostcodeAnywhereInternational/Interactive/ListBuildings/v1.10", { Key: _this.key, StreetId: streetId }, internationalBuildingResults);
        }

        //Clears all fields
        _this.clear = function () {
            for (var i = 0; i < fields.length; i++)
                _pca.setValue(fields[i].element, '');

            _this.resultList.clear();

            _this.fire("clear");
        }

        //Populates the field map with values
        _this.select = function (address) {
            for (var i = 0; i < fields.length; i++)
                _pca.setValue(fields[i].element, _this.getAddressValue(fields[i].field, address));

            if (_this.countryList && _this.countryList.syncCountry)
                _this.countryList.syncCountry(_this.getAddressValue("Country", address));

            _this.fire("select", address);
        }

        //Run setup
        _this.setup();
    }
})(window);


