Is it possible to make a footprint off an assembly of pads in a gEDA PCB layout?
Yes, it is possible. Not natively however, I needed a script. As I guessed, you need to add pads coordinates with the element coordinates — pad coordinates are relative to their containing element.

Here's an example:

Element["" "3.7x4-pad" "" "3.7 x 4 pad" 17.1500mm 11.8500mm 10.00mil 2.6540mm 0 100 ""]
(
    Pad[0.0000 -0.1500mm 0.0000 0.1500mm 3.7000mm 30.00mil 3.7000mm "" "1" "square"]

    )

Element["" "3.7x4-pad" "" "3.7 x 4 pad" 4.3500mm 11.8500mm 3.3540mm 1.5540mm 0 100 ""]
(
    Pad[0.0000 -0.1500mm 0.0000 0.1500mm 3.7000mm 30.00mil 3.7000mm "" "1" "square"]

    )


To make a footprint off this PCB, you need to offset all the pads with the origin of their containing element. I didn't bother doing that by hand, especially if this is likely to become a recurring task. Python scripting to the rescue:

#!/usr/bin/env python

"""
This script parses a PCB file and outputs a comoponent footprint. This is
to be used whenever designers need to build SMD footprints for parts not
yet distributed with gEDA or registered at gedafootprints.org.

Note: the source PCB is assumed valid!
"""
import re
from sys import stdin, stdout, version
from contextlib import contextmanager

# Included in Python 3.4: ignore certain exceptions while doing something
if version &lt; '3.4':

    @contextmanager
    def ignored(*exceptions):
        try:
            yield
        except exceptions:
            pass

# Add numbers with their respective units, forcing millimeter
def add( *items ):
    return "{:.4f}mm".format( sum(items) )


class ComponentFactory(object):

    # Enclose captured matches with parentheses
    descriptors = {
        'PAD': '\s*',
        'SEP': '\s+',
        'DIM': '(-?[0-9.]+\w+)',
        'TXT': '"([^"]*)"',
        'DIG': '(\d)',
        'INT': '(-?\d+)',
        'UINT': '(\d+)',
    }

    # List of fields for a given component
    components = {
        'PCB[]': [
            ('name', 'TXT'),
            ('width', 'DIM'),
            ('height', 'DIM'),
        ],
        'Element[]': [
            ('flags', 'TXT'),
            ('desc', 'TXT'),
            ('name', 'TXT'),
            ('value', 'TXT'),
            ('x' , 'DIM'),
            ('y' , 'DIM'),
            ('tx' , 'DIM'),
            ('ty' , 'DIM'),
            ('dir', 'DIG'),
            ('scale', 'UINT'),
            ('tflags', 'TXT'),
        ],
        'Pad[]': [
            ('x1', 'DIM'),
            ('y1', 'DIM'),
            ('x2', 'DIM'),
            ('y2', 'DIM'),
            ('thick', 'DIM'),
            ('clear', 'DIM'),
            ('mask', 'DIM'),
            ('name', 'TXT'),
            ('number', 'TXT'),
            ('flags', 'TXT'),
        ],
        'Layer()': [
            ('id', 'UINT'),
            ('name', 'TXT'),
        ],
        'Line[]': [
            ('x1', 'DIM'),
            ('y1', 'DIM'),
            ('x2', 'DIM'),
            ('y2', 'DIM'),
            ('thick', 'DIM'),
            ('clear', 'DIM'),
            ('flags', 'TXT'),
        ],
        'Arc[]': [
            ('x' , 'DIM'),
            ('y' , 'DIM'),
            ('width', 'DIM'),
            ('height', 'DIM'),
            ('thick', 'DIM'),
            ('clear', 'DIM'),
            ('angle', 'INT'),
            ('delta', 'INT'),
            ('flags', 'TXT')
        ]
    }

    def __init__(self, name):
        # Figure out the pattern given the name
        pattern = [ n for n in self.components.keys() if n[:-2] == name ][0]

        # Store name and fields for later use
        self.name = name
        self.fields, types = zip(*self.components[pattern])

        # Then compile a regular expression for each matching line
        self.re = re.compile(
            '{}\\{}\s*{}\s*\\{}'.format(name, pattern[-2], self.descriptors['SEP'].join(self.descriptors[t] for t in types), pattern[-1])
        )

    class Component(dict):
        def __getitem__(self, name):
            """ Returns a signed float with fields that have units. Returns
            the raw (text) value otherwise """
            value = super(self.__class__, self).__getitem__(name)
            m = re.match('(-?[0-9.]+)(mil|mm)?', value)
            if not m:
                return value

            if m.group(2) == 'mil':
                return float(m.group(1)) * 0.0254

            return float(m.group(1))

        def __getattr__(self, name):
            """ Return a dict item as if it were an attribute """
            return self.__getitem__(name)

        def __setattr__(self, name, value):
            """ Careful while setting attributes though: check attribute doesn't
            exist before passing it to the dict! """
            try:
                self.__getitem__(name)
                self.__setitem__(name, value)
            except AttributeError:
                super(self.__class__, self).__setattr__(name, value)

    def parse(self, text):
        try:
            return ComponentFactory.Component( zip( self.fields, re.search(self.re, text).groups() ) )

        except AttributeError:
            raise ValueError


# Component parsers
PCB = ComponentFactory('PCB')
Element = ComponentFactory('Element')
Pad = ComponentFactory('Pad')
Layer = ComponentFactory('Layer')
Line = ComponentFactory('Line')
Arc = ComponentFactory('Arc')

# State variables
description = ''
number = 1
origin = ()
offset = ()
silk = False

while 1:
    try:
        text = stdin.readline()
    except KeyboardInterrupt:
        break

    if not text:
        break

    # Strip carriage return
    text = text.strip("\r\n");

    # Use PCB name as element name
    with ignored(ValueError):
        description = PCB.parse(text).name

    # Try parsing an element
    with ignored(ValueError):
        e = Element.parse(text)
        offset = ( e.x, e.y )

        # Mark origin at the middle of the first encountered element
        if not origin:
            origin = -offset[0], -offset[1]

            # Write element header (not sure about usefulness of x and y though)
            stdout.write( '\nElement["" "{name}" "" "" {x}mm {y}mm 10mil 10mil 0 100 ""]\n(\n'.format(name=description, x=offset[0], y=offset[1]) )


    # Try parsing a pad, use last offset to compute pad position
    # Note: first pad by design is always centered around (0, 0)
    with ignored(ValueError):
        p = Pad.parse(text)

        # Offset the pad to the origin
        p.x1, p.y1, p.x2, p.y2 = add(origin[0], p.x1, offset[0]), add(origin[1], p.y1, offset[1]), add(origin[0], p.x2, offset[0]), add(origin[1], p.y2, offset[1])

        # Set pad number
        p.number = number
        number = number + 1
        stdout.write( '\tPad[{x1} {y1} {x2} {y2} {thick} {clear} {mask} "{name}" "{number}" "{flags}"]\n'.format(**p) )

    # Match silk layers to output offset ElementLines
    with ignored(ValueError):
        lay = Layer.parse(text)
        silk = lay.name in ['silk']

    if silk:
        # Transform each line and arc inside the silk layer
        with ignored(ValueError):
            ln = Line.parse(text)
            ln.x1, ln.y1, ln.x2, ln.y2 = add(origin[0], ln.x1), add(origin[1], ln.y1), add(origin[0], ln.x2), add(origin[1], ln.y2)
            stdout.write( '\tElementLine[{x1} {y1} {x2} {y2} {thick}]\n'.format(**ln) )

        with ignored(ValueError):
            arc = Arc.parse(text)
            arc.x, arc.y = add(origin[0], arc.x), add(origin[1], arc.y)
            stdout.write( '\tElementArc[{x} {y} {width} {height} {angle} {delta} {thick}]\n'.format(**arc) )

if description:
    stdout.write( '\n)\n' )


Save the script as build-element.py, for example. To create a footprint off an existing (and valid) PCB:

python build-element.py &lt; your-circuit.pcb &gt; element.fp


You will have to open the generated footprint in pcb, optionally change pin assignment and save the file. Personally I always edit the footprint in a text editor to remove numbers from unused pin, it's faster.

Note the script uses the PCB name as the footprint description. Here's what the final footprint looks like:



The picture shows actual pins in an edited version of the attempt I posted in my question. Here's the footprint file, after I edited the result to manually remove unused pins:

Element["" "Bourns PEC11S SMD Incremental Encoder" "" "" 17.15mm 11.85mm 10mil 10mil 0 100 ""]
(
    Pad[0.0000mm -0.1500mm 0.0000mm 0.1500mm 3.7000mm 30.00mil 3.7000mm "" "" "square"]
    Pad[-12.8000mm -0.1500mm -12.8000mm 0.1500mm 3.7000mm 30.00mil 3.7000mm "" "" "square"]
    Pad[-11.4000mm -8.2500mm -11.4000mm -7.5500mm 2.5000mm 30.00mil 2.5000mm "" "" "square"]
    Pad[-1.4000mm -8.2500mm -1.4000mm -7.5500mm 2.5000mm 30.00mil 2.5000mm "" "" "square"]
    Pad[-3.9000mm -7.5500mm -3.9000mm -6.4500mm 1.3000mm 30.00mil 1.3000mm "E" "5" "square"]
    Pad[-8.9000mm -7.5500mm -8.9000mm -6.4500mm 1.3000mm 30.00mil 1.3000mm "D" "4" "square"]
    Pad[-8.9000mm 6.7500mm -8.9000mm 7.8500mm 1.3000mm 30.00mil 1.3000mm "A" "1" "square"]
    Pad[-6.4000mm 6.7500mm -6.4000mm 7.8500mm 1.3000mm 30.00mil 1.3000mm "C" "2" "square"]
    Pad[-3.9000mm 6.7500mm -3.9000mm 7.8500mm 1.3000mm 30.00mil 1.3000mm "B" "3" "square"]
    Pad[-1.4000mm 7.5500mm -1.4000mm 8.2500mm 2.5000mm 30.00mil 2.5000mm "" "" "square"]
    Pad[-11.4000mm 7.5500mm -11.4000mm 8.2500mm 2.5000mm 30.00mil 2.5000mm "" "" "square"]
    ElementLine[-7.8500mm -8.1500mm -4.9500mm -8.1500mm 6.00mil]
    ElementLine[-7.8500mm 8.4500mm -7.4500mm 8.4500mm 6.00mil]
    ElementLine[-5.3500mm 8.4500mm -5.0500mm 8.4500mm 6.00mil]
    ElementLine[-12.6500mm 2.3500mm -12.6500mm 5.9500mm 6.00mil]
    ElementLine[-0.1500mm 5.9500mm -0.1500mm 2.3500mm 6.00mil]
    ElementLine[-12.6500mm -2.3500mm -12.6500mm -5.9500mm 6.00mil]
    ElementLine[-0.1500mm -5.9500mm -0.1500mm -2.3500mm 6.00mil]

)