javascriptreactjssvgfrontendamcharts

(js) SVG cartography : how to detect border countries


SVG World Map

I am using SVG elements to render this world map in my web app. SVG structure is classic :

<svg id="all_countries">
  <path
    id="india"
    className="st0" // st0 is the default class when exporting svg from Adobe Illustrator
    d="M666.8556,380.5002l0.518,0.3002l-0.0375,-0.0375-0.0891l-0.201-0.1823l0" // country svg drawing
  />

  <path
    id="china"
    className="st0"
    d="M680.55626,380.402l0.518,0.3002l-0.0375,-0.075-0.0591l-0.301-0.3484l2"
  />

  // more countries like Nepal, Kazakhstan, Mongolia, Pakistan etc ...
</svg>

For every country : I am looking for a way to find the border countries. In the following example, all countries are stored in database and have properties :

svgID: "india",
prettyName: "Republic Of India",
population: "1393000000",
currency: "₹",
school: {
  forKids: "free",
  forAdults: "free",
},

I need to add a borderCountries property :

borderCountries: [
  "new ObjectId("2482842848451")", // represents Pakistan
  "new ObjectId("5848564841232")", // represents Nepal
  "new ObjectId("9645485455612")", // represents China
]

I thought about adding them manually to database but (later) I'm adding regions inside countries, and that would be too much manual work.

Any thoughts on how to detect border countries using the SVG elements ?


Solution

  • Update 2025: convert to polygon data and find coinciding points

    When dealing with map data we can make some assumptions:

    Therefore we can take a shortcut by:

    1. parsing all country/map element paths and convert them to polygon vertices:
      1.1 we use the SVG2 pathData interface method getPathData({normalize:true}) that returns all absolute command coordinates – natively supported by Firefox, for Chromium you still need a polyfill (see example)
      1.2 convert the path data to a polygon vertex array: we simply take the first M command values and add each commands final values pair (the final on-path-point)
    2. create a poly lookup for each shape that is populated by these data:
      2.1. Bounding box data: we skip candidates if bounding boxes are not adjacent or intersecting, therefore we don't need to check point proximities in this case.
      2.2. stringified point data saved to a set – we round the coordinates before stringifying them this way we can quickly check if a polygon compared against another shares the same point. The rounding replaces more expensive distance checks to compensate rounding errors produced by path data minifications (conversions to relative or shorthand commands and rounding).

    Overall, the above method creates the neighbor data more than 2 times faster than using point in fill methods (see below).

    function getNeigbourPaths(paths) {
      
      let polys = [];
      let l = paths.length;
      
      // init poly object – convert paths
      for (let i = 0; i < l; i++) {
        let path = paths[i];
        let pathData = path.getPathData({ normalize: true });
        let pts = getPathDataVertices(pathData);
        let bb = getPolyBBox(pts);
    
        //add id if not existent
        let id = path.id ? path.id : "path_" + i;
    
        // lower precision for tolerance
        let precision = 0;
        let scale = 5;
        let pts_set = new Set(
          pts.map((pt) => {
            return `${+(pt.x*scale).toFixed(precision)}_${+(pt.y*scale).toFixed(precision)}`;
          })
        );
    
        polys.push({id, bb, pts_set, neighbors: new Set([]) });
      }
    
      for (let i = 0; i < l; i++) {
        let poly = polys[i];
    
        // vertices1
        let {bb, pts_set}  = poly;
    
        // find neighbours
        for (let j = 0; j < l; j++) {
          
          // same polygon - skip
          if (i === j) continue;
    
          let polyN = polys[j];
          let bb1 = polyN.bb;
          let pts_set1 = polyN.pts_set;
          let id1 = polyN.id;
    
          // check for bounding box intersections
          let intersectBB = checkBBoxIntersections(bb, bb1);
    
          // no intersection possible - skip
          if (!intersectBB) continue;
    
          if (intersectBB) {
            // check against polypoints in set
            for (pt of pts_set1) {
              //console.log(pt, pts_set0)
              if (pts_set.has(pt)) {
                poly.neighbors.add(id1);
                break;
              }
            }
          }
        }
      }
      
      return polys
    }
    
    
    function getPathDataVertices(pathData) {
      let polyPoints = [];
      pathData.forEach((com) => {
        let values = com.values;
        // get final on path point from last 2 values
        if (values.length) {
          let valuesL = values.slice(-2);
          let pt = {
            x: valuesL[0],
            y: valuesL[1]
          };
          polyPoints.push(pt);
        }
      });
      return polyPoints;
    }
    
    function getPolyBBox(vertices) {
      let xArr = vertices.map((pt) => pt.x);
      let yArr = vertices.map((pt) => pt.y);
      let left = Math.min(...xArr);
      let right = Math.max(...xArr);
      let top = Math.min(...yArr);
      let bottom = Math.max(...yArr);
      let bb = {
        x: left,
        left: left,
        right: right,
        y: top,
        top: top,
        bottom: bottom,
        width: right - left,
        height: bottom - top
      };
      return bb;
    }
    
      function checkBBoxIntersections(bb, bb1) {
        let {x,left,y,top,width,height,right,bottom} = bb;
        let [x1, left1, y1, top1, width1, height1, right1, bottom1] = [
          bb1.x,bb1.left,bb1.y,bb1.top,bb1.width,bb1.height,bb1.right,bb1.bottom
        ];
        let intersects = false;
        
        // congruent
        if (width * height === width1 * height1 && x === x1 && y === y1) return true;
    
        if (x <= right1 &&
          right >= x1 &&
          y <= bottom1 &&
          bottom >= y1) {
          intersects = true;
        }
        return intersects;
      }
    svg {
      display: block;
      outline: 1px solid #ccc;
      overflow: visible;
    }
    
    .neighbor{
      fill:red!important;
    }
    <script src="
    https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.10/path-data-polyfill.min.js
    "></script>
    
    <p>Click on states to highlight neighbour states</p>
    <svg xmlns="http://www.w3.org/2000/svg" style="stroke-linejoin:round;stroke:#000;fill:#ccc;stroke-width:0.25%" viewBox="0 0 200 250" id="svg">
      <path id="MA" d="m162.3 142.4-.291-.193v.29l.29-.096zm-2.912-2.62.68-.291v-.388l-.68.68zm12.04-7.57-.097-1.36-.194-.776.29 2.135zm-42.42-9.998-.68.29-5.532 1.651-1.941.68-2.233.679-.776.291v.291l.291 5.048.291 4.659.291 4.27.486.292 1.747-.486 7.862-2.33.194.486 13.98-5.338.097.194 1.262-.486 4.465-1.747 4.27 5.145.583-.486.291-1.456-.097 2.33h.97l.292 1.165.874 1.65 4.562-5.533 3.785 1.262.874-1.941 6.212-3.3-2.621-5.145.68 3.3-3.204 2.427-3.591.291-7.183-7.668-3.203-4.853 3.203-3.397-3.3-.194-1.359-3.204-.097-.194-5.532 6.018-12.23 4.077z" data-id="MA" data-name="Massachusetts" />
      <path id="CT" d="m143 132.7-13.98 5.338-.194-.485-7.862 2.33-1.747.485.194.97 3.688 14.27 1.165 1.456-2.524 3.106 1.748 1.65 5.63-5.435 3.008-4.174.583 1.165 14.17-6.31.292-.29-.097-2.33-.583-2.038-3.008-8.445-.389-1.067z" data-id="CT" data-name="Connecticut" />
      <path id="NH" d="m129.9 65.29-1.262 1.553-1.456-.291-.777 6.406v.097l2.33 8.735-.194 1.748-4.174 5.532 1.068 5.047v6.31l-.777 5.823 4.368 15.92 3.981-1.262 12.23-4.077 5.532-6.018v-1.553l.291-2.523-.679.29-.097-.582-6.115-7.473-.194-.68-14.07-33z" data-id="NH" data-name="New Hampshire" />
      <path id="RI" d="m152.5 141.4-.097-2.038-.29.776zm1.942-1.553-1.262-2.426-.097 2.523zm-.583-3.009.68 1.747.873 1.553.583-1.164-.874-1.65-.291-1.165h-.97v.68zm-10.77-3.98.389 1.068 3.009 8.445.582 2.038.097 2.33.194-.291 4.66-3.689-.874-6.6 1.94-.388-4.27-5.145-4.465 1.747-1.262.486z" data-id="RI" data-name="Rhode Island" />
      <path id="VT" d="m129 122.2-4.369-15.92.777-5.823v-6.31l-1.068-5.047 4.174-5.532.194-1.748-2.33-8.735-1.261.485-5.339 1.941-5.241 2.039-12.04 4.368-.68.194 4.368 10.19 1.456 9.61 5.145 8.056 4.853 15.92.194-.097.776-.291 2.233-.68 1.941-.68 5.533-1.65z" data-id="VT" data-name="Vermont" />
      <path id="NJ" d="m122.8 196.6-.485-1.166v1.165h.485zm.97-3.592.292-2.912-.582 2.524zm-3.979-29.8-10.19-2.523-1.748-.486-1.65-.388-4.076 9.027 1.747 2.135-.097 6.504 1.844.29 3.785 9.901-.194.292-4.756 6.988.485 5.339 11.45 3.882.097 4.465 2.62-3.688.389-5.339 2.718-1.844-1.068-3.98 2.038-4.756v-10.39l-.68-3.397-4.561.097 1.165-4.659.68-7.474z" data-id="NJ" data-name="New Jersey" />
      <path id="NY" d="m119.5 171.1-1.456 2.038-.388.582zm13.4-4.757 4.853-4.27v-.194zm-12.72 3.01v-3.3l-.29 1.746.29 1.553zm22.52-15.53-1.165-.292-.194.874 1.359-.583zm2.62-1.748-.096.97.194-.582-.097-.388zm-2.717.097-6.503 6.018-9.998 4.271-2.62 2.232-.195 2.136-2.717 2.232.388 2.912 4.174-1.65 6.115-4.95 6.794-3.591 10.29-9.707-7.28 5.533-2.33.97zm3.009-4.076.388-.583-1.165 1.36zm-109.1-3.592-1.553-.194 1.165 1.553zm32.22-29.7.194-1.068-.776.97zm.485-6.989-.388.195-.485 1.067zm-41.06 55.13.679 2.912 5.144 1.262 29.51-8.056 29.7-8.93 5.436 4.174 1.068 2.426 6.31 2.718.193.388 1.65.389 1.748.485 10.19 2.523-.194-.485.874 3.98 1.94-.486 1.069-4.367v-.098l-1.748-1.65 2.524-3.106-1.165-1.456-3.688-14.27-.194-.97-.486-.292-.29-4.27-.292-4.66-.291-5.047v-.29l-.194.096-4.853-15.92-5.145-8.056-1.456-9.61-4.368-10.19-.582.29-20.29 6.795-4.077 4.368-4.562 8.736.194 1.941-6.018 9.124 3.689 1.262 1.068 6.891-4.271 5.63-12.81 7.765-2.426-.97-9.512 1.358-8.833 5.339.485 2.815 3.106 2.717-1.747 8.542-6.988 8.153z" data-id="NY" data-name="New York" />
      <path id="PA" d="m106 159.4-6.309-2.718-1.068-2.426-5.435-4.174-29.7 8.93-29.51 8.056-5.144-1.262-.68-2.912-1.65 1.262-2.62 1.844-4.756 4.853-.583.486 2.718 11.94.485 2.136 2.427 10.48.485 2.038 4.271 18.44 13.78-3.397 1.941-.486 1.747-.485 6.892-1.844 37.95-10.58 6.891-2.135 1.65-.486.777-1.94 1.747-1.36h2.33l.29-.873 3.786-4.271.388-.68.291-.194-3.785-9.9-1.844-.291.097-6.504-1.747-2.135 4.076-9.027z" data-id="PA" data-name="Pennsylvania" />
      <path id="ME" d="m152.8 107.7h-.097l.194.096-.097-.097zm7.182-20.67-.776-1.165v.582zm-1.359-1.068v-.97l-.097-.583zm3.495-4.076-.194-.098.097.292zm9.706-4.854-.194-1.456-.388.68zm-2.427.583.097-1.068-1.164.194zm3.592-3.203h-.389v.194zm-4.853 1.456-.874-.195-.097.874.97-.68zm4.95-3.01.873.68-.29-.776-.583.097zm-2.62.098-.874 1.65 1.164-.583zm-4.175.097-.388.582.291.874zm5.727-3.203-.097-.777-.291.486.388.29zm-.485.388-.874-.777v.68zm-5.436 2.232-.194-1.067-.388.873zm9.221-3.397-1.456 2.136-.097-1.553zm7.086-6.503-.68-.97-.388.193zm2.62-4.368-.582-.291-.097.291h.68zm1.262-7.57h-.873l.97.29zm1.456.97-.29-1.747-.777-.097zm-1.553-1.553-.194-1.844-.68 1.261zm-56.3 15.24 14.07 33 .194.68 6.115 7.473v-.097l.97-.29.195-5.533 1.553-1.942.97-5.144-.873-.874 1.65-5.047 5.63-2.62 3.688-4.465 1.359-7.766 5.92-.388-.776-3.98 7.377-2.814.485-4.368 1.747.388 4.854-3.98 2.135-5.144-4.95-4.95-5.242-1.068-1.94-3.98v-2.329l-3.204.97-3.591-1.26v-3.98l-9.707-20.97-7.085-3.689-7.862 6.6-2.136-.387-1.844-3.398-2.524.97-3.591 17.86 1.262 5.824 1.068 11.84-2.815 9.124 1.941 1.456-2.33 4.465-2.523-.388z" data-id="ME" data-name="Maine" />
    </svg>
    
    <p><a href="https://simplemaps.com/resources/svg-us">https://simplemaps.com/resources/svg-us</a></p>
    
    <script>
      window.addEventListener('DOMContentLoaded', e => {
        let paths = document.querySelectorAll("path");
        // get poly neighbors
        let t0 = performance.now();
        let neighborLookup = getNeigbourPaths(paths);
        let t1 = performance.now() - t0;
        console.log('execution time for lookup:', t1);
        
        /**
         * example click event listener
         */
        paths.forEach(path => {
          path.addEventListener('click', e => {
            // reset previous
            resetNeighbors();
            // find path id in lookup
            let item = neighborLookup.filter(item => item.id === path.id)[0];
            let neighbors = item.neighbors;
            neighbors.forEach(id => {
              let el = document.getElementById(id)
              el.classList.add('neighbor')
            })
          })
        })
    
        function resetNeighbors() {
          let els = document.querySelectorAll('.neighbor')
          els.forEach(el => {
            el.classList.remove('neighbor')
          })
        }
      })
    </script>

    Old answer: use SVG pointInStroke()

    You can find border/neighbor states via natively supported js method isPointInStroke().

    let svg = document.querySelector('svg');
    let states = svg.querySelectorAll('path');
    let checks = 0;
    perfStart();
    
    /**
    * collect data in info array
    * find neighbours by pointInStroke checks
    */
    let stateInfo = [];
    let checksPerPath = 12;
    
    states.forEach((state, s) => {
        let id = state.id;
        if (!stateInfo[s]) {
            let bb = state.getBBox();
            stateInfo.push({
                id: id,
                neighbours: [],
                bb: [bb.x, bb.y, bb.x + bb.width, bb.y + bb.height],
                pathLength: state.getTotalLength()
            });
        }
        let [x, y, right, bottom] = stateInfo[s].bb;
        let pathLength = stateInfo[s].pathLength;
    
        for (let i = 0; i < states.length; i++) {
            let state1 = states[i];
            let id = state1.id;
            if (!stateInfo[i]) {
                let bb = state1.getBBox();
                stateInfo.push({
                    id: id,
                    neighbours: [],
                    bb: [bb.x, bb.y, bb.x + bb.width, bb.y + bb.height],
                    pathLength: state1.getTotalLength()
                });
            }
    
            let [x1, y1, right1, bottom1] = stateInfo[i].bb;
            let pathLength1 = stateInfo[i].pathLength;
    
            /**
            * narrow down selection by checking  bbox intersections
            */
            if (
                s != i &&
                x <= right1 &&
                right >= x1 &&
                y <= bottom1 &&
                bottom >= y1) {
    
                /**
                * refine by point in fill checks
                * skip previously compared paths 
                * (e.g already processed A-B combination makes comparing B-A obsolete) 
                */
                if (!stateInfo[s].neighbours.includes(state1.id) && !stateInfo[i].neighbours.includes(state.id)) {
                    let inStroke = false;
                    let inStroke1 = false;
                    for (let c = 0; c < checksPerPath && !inStroke && !inStroke1; c++) {
                        let pt = state.getPointAtLength(pathLength / checksPerPath * c);
                        inStroke = state1.isPointInStroke(pt)
                        // check path 1 against path 2
                        if (inStroke) {
                            stateInfo[s].neighbours.push(state1.id);
                            stateInfo[i].neighbours.push(state.id);
                        }else{
                        // no intersections found: reverse order
                            let pt1 = state1.getPointAtLength(pathLength1 / checksPerPath * c);
                            inStroke1 = state.isPointInStroke(pt1)
                            if (inStroke1) {
                                stateInfo[s].neighbours.push(state1.id);
                                stateInfo[i].neighbours.push(state.id);
                            }
                        }
                        // just for benchmarking
                        checks++
                    }
                }
            }
        }
        let title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
        let neighboursAtt = stateInfo[s].neighbours.join(', ');
        title.textContent = 'Neighbour states: ' + neighboursAtt;
        state.appendChild(title);
        state.dataset.neighbours = neighboursAtt;
    });
    
    
    perfEnd();
    //console.log('total checks:', checks, stateInfo);
    
    /**
     * highlight neighbours on click: test result
     */
    states.forEach(function (state, i) {
        state.addEventListener('click', function (e) {
            removeNeighbourClass();
            let current = e.currentTarget;
            current.classList.add('active');
            let neighbours = e.currentTarget.getAttribute('data-neighbours');
            if (neighbours) {
                let neighboursArr = neighbours.split(', ');
                if (neighboursArr.length) {
                    neighboursArr.forEach(function (el, i) {
                        let neighbour = svg.querySelector('#' + el);
                        neighbour.classList.add('neighbour');
                    })
                }
            }
        });
    });
    
    function removeNeighbourClass() {
        let neighbours = document.querySelectorAll('.neighbour, .active');
        neighbours.forEach(function (el, i) {
            el.classList.remove('active');
            el.classList.remove('neighbour');
        })
    }
    
    
    /**
     * simple performance test
     */
    
    function perfStart() {
        t0 = performance.now();
    }
    
    function perfEnd(text = '') {
        t1 = performance.now();
        total = t1 - t0;
        console.log(`excecution time ${text}:  ${total} ms`);
        return total;
    }
    
    
    function renderPoint(svg, coords, fill = "red", r = "2", opacity = "1", id = "", className = "") {
        //console.log(coords);
        if (Array.isArray(coords)) {
          coords = {
            x: coords[0],
            y: coords[1]
          };
        }
      
        let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
        <title>${coords.x} ${coords.y}</title></circle>`;
      svg.insertAdjacentHTML("beforeend", marker);
      }
    svg{
    max-height:90vmin;
    width:auto;
    }
    
    
    .active{
          fill:blue
    }
    .neighbour{
        fill:#5774ad
    }
    <p>Click on states to highlight neighbour states</p>
    <svg xmlns="http://www.w3.org/2000/svg" style="stroke-linejoin:round;stroke:#000;fill:#ccc" width="1000" viewBox="0 0 200 250" id="svg">
      <path id="MA" d="m162.3 142.4-.291-.193v.29l.29-.096zm-2.912-2.62.68-.291v-.388l-.68.68zm12.04-7.57-.097-1.36-.194-.776.29 2.135zm-42.42-9.998-.68.29-5.532 1.651-1.941.68-2.233.679-.776.291v.291l.291 5.048.291 4.659.291 4.27.486.292 1.747-.486 7.862-2.33.194.486 13.98-5.338.097.194 1.262-.486 4.465-1.747 4.27 5.145.583-.486.291-1.456-.097 2.33h.97l.292 1.165.874 1.65 4.562-5.533 3.785 1.262.874-1.941 6.212-3.3-2.621-5.145.68 3.3-3.204 2.427-3.591.291-7.183-7.668-3.203-4.853 3.203-3.397-3.3-.194-1.359-3.204-.097-.194-5.532 6.018-12.23 4.077z" data-id="MA" data-name="Massachusetts"/>
      <path id="CT" d="m143 132.7-13.98 5.338-.194-.485-7.862 2.33-1.747.485.194.97 3.688 14.27 1.165 1.456-2.524 3.106 1.748 1.65 5.63-5.435 3.008-4.174.583 1.165 14.17-6.31.292-.29-.097-2.33-.583-2.038-3.008-8.445-.389-1.067z" data-id="CT" data-name="Connecticut"/>
      <path id="NH" d="m129.9 65.29-1.262 1.553-1.456-.291-.777 6.406v.097l2.33 8.735-.194 1.748-4.174 5.532 1.068 5.047v6.31l-.777 5.823 4.368 15.92 3.981-1.262 12.23-4.077 5.532-6.018v-1.553l.291-2.523-.679.29-.097-.582-6.115-7.473-.194-.68-14.07-33z" data-id="NH" data-name="New Hampshire"/>
      <path id="RI" d="m152.5 141.4-.097-2.038-.29.776zm1.942-1.553-1.262-2.426-.097 2.523zm-.583-3.009.68 1.747.873 1.553.583-1.164-.874-1.65-.291-1.165h-.97v.68zm-10.77-3.98.389 1.068 3.009 8.445.582 2.038.097 2.33.194-.291 4.66-3.689-.874-6.6 1.94-.388-4.27-5.145-4.465 1.747-1.262.486z" data-id="RI" data-name="Rhode Island"/>
      <path id="VT" d="m129 122.2-4.369-15.92.777-5.823v-6.31l-1.068-5.047 4.174-5.532.194-1.748-2.33-8.735-1.261.485-5.339 1.941-5.241 2.039-12.04 4.368-.68.194 4.368 10.19 1.456 9.61 5.145 8.056 4.853 15.92.194-.097.776-.291 2.233-.68 1.941-.68 5.533-1.65z" data-id="VT" data-name="Vermont"/>
      <path id="NJ" d="m122.8 196.6-.485-1.166v1.165h.485zm.97-3.592.292-2.912-.582 2.524zm-3.979-29.8-10.19-2.523-1.748-.486-1.65-.388-4.076 9.027 1.747 2.135-.097 6.504 1.844.29 3.785 9.901-.194.292-4.756 6.988.485 5.339 11.45 3.882.097 4.465 2.62-3.688.389-5.339 2.718-1.844-1.068-3.98 2.038-4.756v-10.39l-.68-3.397-4.561.097 1.165-4.659.68-7.474z" data-id="NJ" data-name="New Jersey"/>
      <path id="NY" d="m119.5 171.1-1.456 2.038-.388.582zm13.4-4.757 4.853-4.27v-.194zm-12.72 3.01v-3.3l-.29 1.746.29 1.553zm22.52-15.53-1.165-.292-.194.874 1.359-.583zm2.62-1.748-.096.97.194-.582-.097-.388zm-2.717.097-6.503 6.018-9.998 4.271-2.62 2.232-.195 2.136-2.717 2.232.388 2.912 4.174-1.65 6.115-4.95 6.794-3.591 10.29-9.707-7.28 5.533-2.33.97zm3.009-4.076.388-.583-1.165 1.36zm-109.1-3.592-1.553-.194 1.165 1.553zm32.22-29.7.194-1.068-.776.97zm.485-6.989-.388.195-.485 1.067zm-41.06 55.13.679 2.912 5.144 1.262 29.51-8.056 29.7-8.93 5.436 4.174 1.068 2.426 6.31 2.718.193.388 1.65.389 1.748.485 10.19 2.523-.194-.485.874 3.98 1.94-.486 1.069-4.367v-.098l-1.748-1.65 2.524-3.106-1.165-1.456-3.688-14.27-.194-.97-.486-.292-.29-4.27-.292-4.66-.291-5.047v-.29l-.194.096-4.853-15.92-5.145-8.056-1.456-9.61-4.368-10.19-.582.29-20.29 6.795-4.077 4.368-4.562 8.736.194 1.941-6.018 9.124 3.689 1.262 1.068 6.891-4.271 5.63-12.81 7.765-2.426-.97-9.512 1.358-8.833 5.339.485 2.815 3.106 2.717-1.747 8.542-6.988 8.153z" data-id="NY" data-name="New York"/>
      <path id="PA" d="m106 159.4-6.309-2.718-1.068-2.426-5.435-4.174-29.7 8.93-29.51 8.056-5.144-1.262-.68-2.912-1.65 1.262-2.62 1.844-4.756 4.853-.583.486 2.718 11.94.485 2.136 2.427 10.48.485 2.038 4.271 18.44 13.78-3.397 1.941-.486 1.747-.485 6.892-1.844 37.95-10.58 6.891-2.135 1.65-.486.777-1.94 1.747-1.36h2.33l.29-.873 3.786-4.271.388-.68.291-.194-3.785-9.9-1.844-.291.097-6.504-1.747-2.135 4.076-9.027z" data-id="PA" data-name="Pennsylvania"/>
      <path id="ME" d="m152.8 107.7h-.097l.194.096-.097-.097zm7.182-20.67-.776-1.165v.582zm-1.359-1.068v-.97l-.097-.583zm3.495-4.076-.194-.098.097.292zm9.706-4.854-.194-1.456-.388.68zm-2.427.583.097-1.068-1.164.194zm3.592-3.203h-.389v.194zm-4.853 1.456-.874-.195-.097.874.97-.68zm4.95-3.01.873.68-.29-.776-.583.097zm-2.62.098-.874 1.65 1.164-.583zm-4.175.097-.388.582.291.874zm5.727-3.203-.097-.777-.291.486.388.29zm-.485.388-.874-.777v.68zm-5.436 2.232-.194-1.067-.388.873zm9.221-3.397-1.456 2.136-.097-1.553zm7.086-6.503-.68-.97-.388.193zm2.62-4.368-.582-.291-.097.291h.68zm1.262-7.57h-.873l.97.29zm1.456.97-.29-1.747-.777-.097zm-1.553-1.553-.194-1.844-.68 1.261zm-56.3 15.24 14.07 33 .194.68 6.115 7.473v-.097l.97-.29.195-5.533 1.553-1.942.97-5.144-.873-.874 1.65-5.047 5.63-2.62 3.688-4.465 1.359-7.766 5.92-.388-.776-3.98 7.377-2.814.485-4.368 1.747.388 4.854-3.98 2.135-5.144-4.95-4.95-5.242-1.068-1.94-3.98v-2.329l-3.204.97-3.591-1.26v-3.98l-9.707-20.97-7.085-3.689-7.862 6.6-2.136-.387-1.844-3.398-2.524.97-3.591 17.86 1.262 5.824 1.068 11.84-2.815 9.124 1.941 1.456-2.33 4.465-2.523-.388z" data-id="ME" data-name="Maine"/>
    </svg>
    
    <p><a href="https://simplemaps.com/resources/svg-us">https://simplemaps.com/resources/svg-us</a></p>

    How it works