I used the assembler (NASM) in the 32-bit non-segmented mode,
where all addresses are simply 32-bit offsets.
The linker, startup code (AKA loader) and generated code then
convert these into seg16:ofs16 using 80386 32-bit instructions.
[This is SmallerC's huge memory model in a nutshell.]
While said conversion is conceptually trivial, only a few 8086
instructions (far call, far jump, lds/les) can consume 32 bits
of an address (seg16:ofs16). Everywhere else you have the far
address split into two 16-bit portions with some space (code)
between the two.
If you want to be able to use these 16-bit parts independently,
you need to produce two special kinds of relocation records,
one for the SEG keyword, the other for the OFS/OFFSET.
The latter is more or less trivial and already exists, but is
probably 32-bit in your assembler, whereas it needs to be 16-bit.
The former needs to be introduced.
The simplest is perhaps to restrict every object file to
two segments/sections (code and data), each at most 64KB
in size and 16-byte aligned and padded to a multiple of 16 bytes.
OFS/OFFSET would then be the subroutine/variable offset
from the beginning of its segment/section in the object file.
SEG would simply account for the cumulative size of all
the linked object files. In essence, you end up relocating
only the SEG part as the OFS/OFFSET part is fixed.
Otherwise you could dump all far pointers into the default
data segment and simply do lds/les as needed (naturally,
the pointer values must've been relocated by then).